From 930fbf8b7f081c8f6f43c1fb42d436bec035c22e Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sat, 22 Jun 2019 15:02:49 +0300 Subject: [PATCH 01/71] 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 4752ca559a0dd591ce1623f66dc50f7db1bb3a42 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sun, 23 Jun 2019 14:56:54 +0300 Subject: [PATCH 02/71] 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 972fd8cca024ad4d9d5c919f481f0c83aef99e68 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sun, 23 Jun 2019 23:02:23 +0300 Subject: [PATCH 03/71] 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 5f03b3870fd9bb876eec66b897c630e8e724547b Mon Sep 17 00:00:00 2001 From: Rampastring Date: Sun, 23 Jun 2019 23:44:02 +0300 Subject: [PATCH 04/71] Implement starting games with V3 tunnels, testing TODO --- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 4 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 262 ++++++++++++++++-- .../Multiplayer/CnCNet/GameTunnelHandler.cs | 11 +- 3 files changed, 247 insertions(+), 30 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index c6b359014..3b2c9fce1 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -892,7 +892,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; @@ -1004,7 +1004,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 c3d058cc5..8c3bf196c 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; @@ -163,6 +189,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; @@ -172,6 +202,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; @@ -189,12 +225,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() @@ -209,7 +251,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; @@ -224,6 +266,7 @@ public void SetUp(Channel channel, bool isHost, int playerLimit, this.hostName = hostName; this.playerLimit = playerLimit; this.isCustomPassword = isCustomPassword; + this.isP2P = isP2P; if (isHost) { @@ -550,6 +593,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) @@ -627,38 +672,27 @@ protected override void HostLaunchGame() { if (Players.Count > 1) { - AddNotice("Contacting tunnel server...".L10N("UI: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." + Environment.NewLine + - "Try picking a different tunnel server:").L10N("UI:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + - "tunnel server. Please try using a different tunnel server ").L10N("UI: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(); @@ -667,6 +701,175 @@ protected override void HostLaunchGame() StartGame(); } + private void StartGame_V2Tunnel() + { + AddNotice("Contacting tunnel server...".L10N("UI:Main:ConnectingTunnel")); + + List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count); + + if (playerPorts.Count < Players.Count) + { + ShowTunnelSelectionWindow(("An error occured while contacting " + + "the CnCNet tunnel server." + Environment.NewLine + + "Try picking a different tunnel server:").L10N("UI: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("UI: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[] @@ -1240,6 +1443,9 @@ protected override void GameProcessExited() /// private void NonHostLaunchGame(string sender, string message) { + if (tunnel.Version != Constants.TUNNEL_VERSION_2) + return; + if (sender != hostName) return; @@ -1288,6 +1494,8 @@ protected override void StartGame() { AddNotice("Starting game...".L10N("UI: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 6869e46f0b62ea71b4b7e0daf732b33bed7c45f2 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Mon, 24 Jun 2019 23:26:03 +0300 Subject: [PATCH 05/71] 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 6bf23f6889a7e41e3313c2c02579aa9e3ff65891 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Mon, 24 Jun 2019 23:27:28 +0300 Subject: [PATCH 06/71] 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 22e45616a..df1298ef9 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -29,7 +29,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 8c3bf196c..d64c864bc 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -205,7 +205,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); @@ -234,6 +233,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(); } @@ -738,7 +753,6 @@ private void StartGame_V2Tunnel() private void StartGame_V3Tunnel() { - AddNotice("Contacting tunnel server.."); btnLaunchGame.InputEnabled = false; Random random = new Random(); @@ -790,6 +804,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)]); @@ -800,12 +815,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); @@ -856,6 +881,7 @@ private void AbortGameStart() { btnLaunchGame.InputEnabled = true; gameTunnelHandler.Clear(); + gameStartTimer.Pause(); isStartingGame = false; } From 29bc63e4aafafc45f13333a11a5c1756ee6e6772 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Mon, 24 Jun 2019 23:29:55 +0300 Subject: [PATCH 07/71] 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 b7b39afbb..b144d3d25 100644 --- a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs +++ b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs @@ -913,7 +913,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 = "-"; @@ -942,7 +942,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(); @@ -956,6 +956,13 @@ private void SetTotalStatistics() lblAverageAILevelValue.Text = "Hard".L10N("UI: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 b2fb00fa7e7e1e09892269f043bdb6b982fbf3f2 Mon Sep 17 00:00:00 2001 From: Rampastring Date: Tue, 25 Jun 2019 20:31:49 +0300 Subject: [PATCH 08/71] 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 d64c864bc..1d523c7b0 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -867,11 +867,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(); } @@ -1539,8 +1542,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 69780ca1e12a3791078c199d82d841985491eacb Mon Sep 17 00:00:00 2001 From: Rampastring Date: Tue, 25 Jun 2019 21:23:48 +0300 Subject: [PATCH 09/71] 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 1d523c7b0..5248823c8 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; @@ -868,7 +870,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 cd19c6c710d426b733efdf231e9644ab5f5afa23 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 14 Aug 2022 16:03:51 +0200 Subject: [PATCH 10/71] 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 5248823c8..28bcea20f 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -692,11 +692,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(); } @@ -808,7 +808,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 @@ -897,7 +897,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); @@ -1476,7 +1476,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) @@ -1546,7 +1546,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 f2b51e1d7914f5dd6f959903d7f430e8fa57f50c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 14 Aug 2022 19:34:23 +0200 Subject: [PATCH 11/71] 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 | 16 +- .../Multiplayer/CnCNet/TunnelHandler.cs | 32 ++-- .../CnCNet/TunneledPlayerConnection.cs | 16 +- .../Multiplayer/CnCNet/V3TunnelConnection.cs | 63 +------- 11 files changed, 80 insertions(+), 236 deletions(-) diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 38dea53ca..1170d5953 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; @@ -724,4 +724,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 3b2c9fce1..4201aa46b 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 e54fb4f14..7dc8aca9a 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 28bcea20f..1a84413d9 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; @@ -245,7 +245,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. " + @@ -692,6 +692,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(); @@ -720,8 +722,6 @@ protected override void HostLaunchGame() private void StartGame_V2Tunnel() { - AddNotice("Contacting tunnel server...".L10N("UI:Main:ConnectingTunnel")); - List playerPorts = tunnelHandler.CurrentTunnel.GetPlayerPortInfo(Players.Count); if (playerPorts.Count < Players.Count) @@ -806,9 +806,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 496d6ac5e..cf1f26c1c 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; @@ -33,9 +29,9 @@ public HostedCnCNetGame(string channelName, string revision, string gamever, int public override int Ping => TunnelServer.PingInMs; - public override bool Equals(GenericHostedGame other) => - other is HostedCnCNetGame hostedCnCNetGame ? + public override bool Equals(GenericHostedGame other) => + other is HostedCnCNetGame hostedCnCNetGame ? 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 2583ebeb88b1ce210e63598e98b938902c395f06 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 15 Aug 2022 12:07:44 +0200 Subject: [PATCH 12/71] 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 df1298ef9..d3c1328f2 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -31,7 +31,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 1a84413d9..367b455ff 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 Localization; @@ -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; @@ -870,7 +870,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++) @@ -883,6 +883,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 a8746f784da6d458a73d8620b95350a472220cdd Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 15 Aug 2022 15:32:55 +0200 Subject: [PATCH 13/71] 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 1170d5953..e0739abac 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -639,6 +639,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 c843a1c7d..0f3fe504a 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); @@ -473,8 +475,6 @@ protected void CopyPlayerDataToUI() } } - protected virtual string GetIPAddressForPlayer(PlayerInfo pInfo) => "0.0.0.0"; - private void DdSavedGame_SelectedIndexChanged(object sender, EventArgs e) { if (!IsHost) @@ -521,4 +521,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 367b455ff..b1fcc2b81 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -778,6 +778,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; @@ -847,6 +849,8 @@ private void HandleTunnelFail(string playerName) private void HandleTunnelConnected(string playerName) { + Logger.Log($"Tunnel_V3 received TNLOK {playerName}."); + if (!isStartingGame) return; @@ -871,10 +875,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 cef70dfa5..b5ee24d28 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1318,12 +1318,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 0d038a644691cdd651aa7340a2ead9dc79c4e050 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 15 Aug 2022 23:33:03 +0200 Subject: [PATCH 14/71] 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 b1fcc2b81..05e68bf29 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -874,8 +874,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++) @@ -883,6 +881,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 95a7673872bcd134494d2a7f7fb8f15a6690c749 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 16 Aug 2022 22:39:13 +0200 Subject: [PATCH 15/71] 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 0229282d7..21e8ad1a3 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 e0739abac..1170d5953 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs @@ -639,7 +639,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 0f3fe504a..8670e714a 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 05e68bf29..4009cc309 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 Localization; @@ -778,8 +776,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; @@ -849,8 +845,6 @@ private void HandleTunnelFail(string playerName) private void HandleTunnelConnected(string playerName) { - Logger.Log($"Tunnel_V3 received TNLOK {playerName}."); - if (!isStartingGame) return; @@ -875,36 +869,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 b5ee24d28..d84988cfd 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1320,7 +1320,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 1246f2e1a106d81fee80ae27d307029f560f896a Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 17 Aug 2022 20:05:32 +0200 Subject: [PATCH 16/71] 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 d84988cfd..cef70dfa5 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1318,13 +1318,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 c9c7297e8f022e9fac43bb7ec05bca2813a89a2a Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 19 Aug 2022 16:57:35 +0200 Subject: [PATCH 17/71] 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 e5bfeaf711377a57f054477d6ee4fbac68a51b1b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 19 Aug 2022 17:01:07 +0200 Subject: [PATCH 18/71] 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 abbf0162943075f8e0de388ea08f40877536a5f7 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 23:33:46 +0200 Subject: [PATCH 19/71] 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 1170d5953..5e33a43fe 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 5407f9a7d..69018dbcc 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 7f78a6460..6dca0c65c 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 4009cc309..b4df96cdb 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -384,10 +384,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 ec6d4ff972a7c272cef80c3fa1a77064745c4f04 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 20 Aug 2022 23:50:30 +0200 Subject: [PATCH 20/71] 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 | 485 +++--- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 823 ++++++---- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 71 +- .../CnCNet/PrivateMessagingWindow.cs | 72 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 165 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 1462 ++++++++++------- .../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, 4549 insertions(+), 3534 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 acad5d783..50015016e 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 5e2c26543..9992cf5b9 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; namespace DTAClient.DXGUI.Generic @@ -181,7 +182,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); @@ -495,7 +496,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) { @@ -528,17 +529,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 2a309493e..3177ede8b 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("UI:Main:LogOut"); 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 fb8fe270d..78bce3447 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("UI:Main:ForceUpdateFailureTitle"), "Checking for updates failed.".L10N("UI: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 5e33a43fe..a25b92dab 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("UI: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("UI: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("UI:Main:PlayerCheating"), cheaterName), Color.Red); + AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("UI: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("UI: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("UI: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,44 +659,50 @@ private void HandleTunnelServerChange(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("UI:Main:HostChangeTunnel"), tunnel.Name)); - //UpdatePing(); } #endregion - protected override void HostStartGame() + protected override async Task HostStartGameAsync() { - AddNotice("Contacting tunnel server...".L10N("UI: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." + Environment.NewLine + - "Try picking a different tunnel server:").L10N("UI:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + - "tunnel server. Please try using a different tunnel server ").L10N("UI:Main:ConnectTunnelError2"), Color.Yellow); - return; + AddNotice("Contacting tunnel server...".L10N("UI: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." + Environment.NewLine + + "Try picking a different tunnel server:").L10N("UI:Main:ConnectTunnelError1")); + AddNotice(("An error occured while contacting the specified CnCNet " + + "tunnel server. Please try using a different tunnel server ").L10N("UI: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("UI: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("UI:Main:StartingGame")); - - started = true; - - LoadGame(); } protected override void WriteSpawnIniAdditions(IniFile spawnIni) @@ -642,14 +713,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) { @@ -657,50 +735,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(lblMapNameValue.Text); + sb.Append(";"); + sb.Append(lblGameModeValue.Text); + 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(lblMapNameValue.Text); - sb.Append(";"); - sb.Append(lblGameModeValue.Text); - 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("UI:Main:LoadGame"); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 4201aa46b..f75578c8d 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -18,8 +18,8 @@ using System.Linq; using System.Reflection; using System.Threading; +using System.Threading.Tasks; using ClientCore.Enums; -using DTAConfig; using Localization; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; @@ -37,8 +37,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) + GameCollection gameCollection, CnCNetUserData cncnetUserData) : base(windowManager) { this.connectionManager = connectionManager; @@ -49,20 +48,18 @@ public CnCNetLobby(WindowManager windowManager, CnCNetManager connectionManager, this.pmWindow = pmWindow; this.gameCollection = gameCollection; this.cncnetUserData = cncnetUserData; - this.optionsWindow = optionsWindow; 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 CnCNetManager connectionManager; - private CnCNetUserData cncnetUserData; - private readonly OptionsWindow optionsWindow; + private readonly CnCNetManager connectionManager; + private readonly CnCNetUserData cncnetUserData; private PlayerListBox lbPlayerList; private ChatListBox lbChatMessages; @@ -93,50 +90,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 CnCNetGameLobby gameLobby; - private CnCNetGameLoadingLobby gameLoadingLobby; + private readonly CnCNetGameLobby gameLobby; + private readonly 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; @@ -179,7 +179,7 @@ public override void Initialize() btnLogout.ClientRectangle = new Rectangle(Width - 145, btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnLogout.Text = "Log Out".L10N("UI:Main:ButtonLogOut"); - btnLogout.LeftClick += BtnLogout_LeftClick; + btnLogout.LeftClick += (_, _) => BtnLogout_LeftClickAsync(); var gameListRectangle = new Rectangle( btnNewGame.X, 41, @@ -211,7 +211,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); @@ -220,7 +220,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); @@ -231,7 +231,7 @@ public override void Initialize() tbChatInput.Suggestion = "Type here to chat...".L10N("UI:Main:ChatHere"); tbChatInput.Enabled = false; tbChatInput.MaximumTextLength = 200; - tbChatInput.EnterPressed += TbChatInput_EnterPressed; + tbChatInput.EnterPressed += (_, _) => TbChatInput_EnterPressedAsync(); lblColor = new XNALabel(WindowManager); lblColor.Name = nameof(lblColor); @@ -244,8 +244,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) @@ -272,7 +270,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); @@ -353,13 +351,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(); @@ -515,16 +519,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; @@ -538,8 +538,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(); @@ -547,7 +547,7 @@ private void PostUIInit() string.Format("*** DTA CnCNet Client version {0} ***".L10N("UI: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; @@ -572,79 +572,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("UI:Main:PlayerBannedByChannel"), chatChannel.UIName))); - return; - } + 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("UI: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("UI:Main:PlayerBannedByHost"), game.RoomName))); + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, string.Format( + "Cannot join game {0}, you've been banned by the game host!".L10N("UI:Main:PlayerBannedByHost"), game.RoomName))); - isJoiningGame = false; - if (gameOfLastJoinAttempt != null) + 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())) - { - connectionManager.FindChannel(game.GameBroadcastChannel).Leave(); - followedGames.Remove(game.InternalName); - } - else if (!followedGames.Contains(game.InternalName) && - UserINISettings.Instance.IsGameFollowed(game.InternalName.ToUpper())) + foreach (CnCNetGame game in gameCollection.GameList) { - 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) @@ -748,11 +762,11 @@ private void SetLogOutButtonText() btnLogout.Text = "Log Out".L10N("UI: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() { @@ -797,16 +811,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)) @@ -815,7 +836,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); } /// @@ -825,7 +846,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)) @@ -868,65 +889,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("UI:Main:AttemptJoin"), hg.RoomName))); - isJoiningGame = true; - gameOfLastJoinAttempt = hg; + try + { + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Attempting to join game {0} ...".L10N("UI: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("UI:Main:GameLocked"))); - var channel = (Channel)sender; + try + { + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "The selected game is locked!".L10N("UI:Main:GameLocked"))); + var channel = (Channel)sender; - var game = FindGameByChannelName(channel.ChannelName); - if (game != null) + var game = FindGameByChannelName(channel.ChannelName); + if (game != null) + { + game.Locked = true; + SortAndRefreshHostedGames(); + } + + await ClearGameJoinAttemptAsync((Channel)sender); + } + catch (Exception ex) { - game.Locked = true; - SortAndRefreshHostedGames(); + PreStartup.HandleException(ex); } - - ClearGameJoinAttempt((Channel)sender); } private HostedCnCNetGame FindGameByChannelName(string channelName) @@ -938,38 +979,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("UI:Main:PasswordWrong"))); - ClearGameJoinAttempt((Channel)sender); + try + { + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("UI: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; } @@ -987,78 +1035,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; + + 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; + } - 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("UI: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; + //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("UI: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 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("UI: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("UI: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; } @@ -1081,16 +1143,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() @@ -1133,40 +1202,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) @@ -1180,95 +1256,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("UI:Main:GameInviteTitle"), - GetUserTexture(sender), - sender, - string.Format("Join {0}?".L10N("UI:Main:GameInviteText"), gameName), - "Yes".L10N("UI:Main:ButtonYes"), "No".L10N("UI: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("UI:Main:GameInviteTitle"), + GetUserTexture(sender), + sender, + string.Format("Join {0}?".L10N("UI:Main:GameInviteText"), gameName), + "Yes".L10N("UI:Main:ButtonYes"), "No".L10N("UI: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("UI:Main:JoinFailedTitle"), + string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("UI: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("UI:Main:JoinFailedTitle"), - string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("UI: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) @@ -1285,53 +1375,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); } } @@ -1548,21 +1645,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() @@ -1640,14 +1744,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); } @@ -1669,22 +1769,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("UI: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("UI: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("UI: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("UI: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 6a53ada53..1009258b0 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 79faf308b..8db590900 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 Localization; 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 8670e714a..8dc7d96bd 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("UI: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("UI: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("UI: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.Text = spawnSGIni.GetStringValue("Settings", "UIMapName", string.Empty); lblGameModeValue.Text = spawnSGIni.GetStringValue("Settings", "UIGameMode", string.Empty); @@ -473,37 +498,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 b4df96cdb..23dadb62a 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 Localization; @@ -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; public override void Initialize() { @@ -214,7 +217,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); @@ -228,6 +231,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(); } @@ -254,7 +265,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 @@ -263,20 +274,20 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI: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; @@ -286,7 +297,7 @@ public void SetUp(Channel channel, bool isHost, int playerLimit, if (isHost) { RandomSeed = new Random().Next(); - RefreshMapSelectionUI(); + await RefreshMapSelectionUIAsync(); btnChangeTunnel.Enable(); } else @@ -299,15 +310,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); @@ -316,12 +327,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)); @@ -332,29 +343,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); } } @@ -386,11 +404,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) @@ -399,20 +424,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) { @@ -423,8 +448,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; @@ -440,26 +465,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) @@ -475,7 +515,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) { @@ -496,41 +536,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("UI: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("UI: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("UI: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("UI: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("UI:Main:YouWereKicked"))); - Clear(); + await ClearAsync(); this.Visible = false; this.Enabled = false; return; @@ -547,63 +605,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("UI: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("UI: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("UI:Main:GameRoomNumberLimitReached")); + await LockGameAsync(); + } + } + catch (Exception ex) { - AddNotice("Player limit reached. The game room has been locked.".L10N("UI:Main:GameRoomNumberLimitReached")); - LockGame(); + PreStartup.HandleException(ex); } } - private void RemovePlayer(string playerName) + private async Task RemovePlayerAsync(string playerName) { AbortGameStart(); @@ -617,15 +689,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) @@ -680,44 +750,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("UI: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) { @@ -741,14 +818,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; @@ -765,7 +842,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(); @@ -812,24 +889,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) @@ -840,7 +931,7 @@ private void HandleTunnelFail(string playerName) AbortGameStart(); } - private void HandleTunnelConnected(string playerName) + private async Task HandleTunnelConnectedAsync(string playerName) { if (!isStartingGame) return; @@ -875,7 +966,7 @@ private void HandleTunnelConnected(string playerName) Players.Single(p => p.Name == ProgramConstants.PLAYERNAME).Port = ports.Item2; gameStartTimer.Pause(); btnLaunchGame.InputEnabled = true; - StartGame(); + await StartGameAsync(); } } @@ -898,10 +989,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, @@ -910,12 +1000,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) { @@ -923,7 +1013,7 @@ protected override void RequestReadyStatus() "you will be unable to participate in the match.").L10N("UI: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; } @@ -936,7 +1026,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)); @@ -944,92 +1034,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 "); @@ -1065,23 +1170,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); } /// @@ -1188,9 +1302,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; @@ -1230,180 +1344,186 @@ protected override void OnGameOptionChanged() sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.Name); - 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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:HostEnableOption"), checkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("UI: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("UI: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("UI:Main:HostEnableOption"), checkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("UI: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("UI: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("UI: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("UI:Main:HostGameOptionInvalidTheSecondTime"), 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("UI: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("UI:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); + } - AddNotice(string.Format("The game host has set {0} to {1}".L10N("UI: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("UI: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("UI: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) { @@ -1415,16 +1535,16 @@ private void RequestMap(string mapSHA1) AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("UI: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("UI: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("UI: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) @@ -1435,90 +1555,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); + + await StartGameAsync(); - StartGame(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } - protected override void StartGame() + protected override async Task StartGameAsync() { AddNotice("Starting game...".L10N("UI:Main:StartingGame")); @@ -1530,11 +1665,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) @@ -1558,7 +1693,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 @@ -1578,80 +1713,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) @@ -1677,21 +1875,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); } } @@ -1703,28 +1910,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("UI: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("UI:Main:RoomLockedByYou")); - LockGame(); + await LockGameAsync(); } else { if (Players.Count < playerLimit) { AddNotice("You've unlocked the game room.".L10N("UI:Main:RoomUnockedByYou")); - UnlockGame(false); + await UnlockGameAsync(false); } else AddNotice(string.Format( @@ -1732,20 +1939,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("UI: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) @@ -1754,7 +1961,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; @@ -1762,10 +1969,10 @@ protected override void KickPlayer(int playerIndex) var pInfo = Players[playerIndex]; AddNotice(string.Format("Kicking {0} from the game...".L10N("UI: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; @@ -1777,142 +1984,178 @@ protected override void BanPlayer(int playerIndex) if (user != null) { AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("UI: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("UI: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) + 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! " + + "The game host needs to change the server or you will be unable " + + "to participate in the match.").L10N("UI:Main:HostInvalidTunnel"), + Color.Yellow); + btnLaunchGame.AllowClick = false; + return; + } + + await HandleTunnelServerChangeAsync(tunnel); + btnLaunchGame.AllowClick = true; + } + catch (Exception ex) { - 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("UI:Main:HostInvalidTunnel"), - Color.Yellow); - btnLaunchGame.AllowClick = false; - return; + PreStartup.HandleException(ex); } - - HandleTunnelServerChange(tunnel); - btnLaunchGame.AllowClick = true; } /// /// 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("UI:Main:HostChangeTunnel"), tunnel.Name)); - UpdatePing(); + return UpdatePingAsync(); } #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("UI: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("UI: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("UI:Main:DownloadMapCommandFailedGeneric")); + mapSharingConfirmationPanel.SetFailedStatus(); + return; + } + + AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("UI: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("UI:Main:DownloadMapCommandFailedGeneric")); - mapSharingConfirmationPanel.SetFailedStatus(); - return; + PreStartup.HandleException(ex); } - - AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:UpdateMapToDBFailed"), map.Name)); - if (map == Map) + AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("UI: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("UI: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("UI: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("UI:Main:UpdateMapToDBSuccess"), e.Map.Name)); - if (e.Map == Map) + AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("UI: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); } } @@ -2104,60 +2347,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.Name); - sb.Append(";"); - sb.Append(GameMode.UIName); - 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.Name); + sb.Append(";"); + sb.Append(GameMode.UIName); + 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("UI: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 cef70dfa5..d7ea02b7d 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; @@ -176,6 +177,8 @@ protected GameModeMap GameModeMap private LoadOrSaveGameOptionPresetWindow loadOrSaveGameOptionPresetWindow; + private XNAMultiColumnListBox.SelectedIndexChangedEventHandler lbGameModeMapList_SelectedIndexChangedFunc; + public override void Initialize() { Name = _iniSectionName; @@ -203,10 +206,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"); @@ -219,7 +222,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 @@ -247,7 +250,7 @@ public override void Initialize() lbGameModeMapList.AddColumn("MAP NAME".L10N("UI: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) @@ -263,8 +266,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(); } @@ -383,52 +388,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("UI:Main:PresetLoaded")); - else - AddNotice(string.Format("Preset {0} not found!".L10N("UI:Main:PresetNotFound"), presetName)); + try + { + if (await LoadGameOptionPresetAsync(presetName)) + AddNotice("Game option preset loaded succesfully.".L10N("UI:Main:PresetLoaded")); + else + AddNotice(string.Format("Preset {0} not found!".L10N("UI: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; @@ -440,7 +468,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) @@ -502,7 +530,7 @@ private List GetSortedGameModeMaps() protected void ListMaps() { - lbGameModeMapList.SelectedIndexChanged -= LbGameModeMapList_SelectedIndexChanged; + lbGameModeMapList.SelectedIndexChanged -= lbGameModeMapList_SelectedIndexChangedFunc; lbGameModeMapList.ClearItems(); lbGameModeMapList.SetTopIndex(0); @@ -567,7 +595,7 @@ protected void ListMaps() lbGameModeMapList.TopIndex++; } - lbGameModeMapList.SelectedIndexChanged += LbGameModeMapList_SelectedIndexChanged; + lbGameModeMapList.SelectedIndexChanged += lbGameModeMapList_SelectedIndexChangedFunc; } protected abstract int GetDefaultMapRankIndex(GameModeMap gameModeMap); @@ -599,7 +627,7 @@ private void DeleteMapConfirmation() var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Delete Confirmation".L10N("UI:Main:DeleteMapConfirmTitle"), string.Format("Are you sure you wish to delete the custom map {0}?".L10N("UI:Main:DeleteMapConfirmText"), Map.Name)); - messageBox.YesClickedAction = DeleteSelectedMap; + messageBox.YesClickedAction = _ => DeleteSelectedMapAsync(); } private void MapPreviewBox_ToggleFavorite(object sender, EventArgs e) => @@ -624,7 +652,7 @@ protected void RefreshForFavoriteMapRemoved() lbGameModeMapList.SelectedIndex = 0; // the map was removed while viewing favorites } - private void DeleteSelectedMap(XNAMessageBox messageBox) + private async Task DeleteSelectedMapAsync() { try { @@ -643,7 +671,7 @@ private void DeleteSelectedMap(XNAMessageBox messageBox) } ListMaps(); - ChangeMap(GameModeMap); + await ChangeMapAsync(GameModeMap); } catch (IOException ex) { @@ -651,40 +679,59 @@ private void DeleteSelectedMap(XNAMessageBox messageBox) XNAMessageBox.Show(WindowManager, "Deleting Map Failed".L10N("UI:Main:DeleteMapFailedTitle"), "Deleting map failed! Reason:".L10N("UI: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 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) @@ -692,15 +739,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; @@ -711,7 +758,7 @@ protected void RefreshMapSelectionUI() return; if (ddGameModeMapFilter.SelectedIndex == gameModeMapFilterIndex) - DdGameModeMapFilter_SelectedIndexChanged(this, EventArgs.Empty); + await DdGameModeMapFilter_SelectedIndexChangedAsync(); ddGameModeMapFilter.SelectedIndex = gameModeMapFilterIndex; } @@ -760,7 +807,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; @@ -775,7 +822,7 @@ protected void InitPlayerOptionDropdowns() foreach (string sideName in sides) ddPlayerSide.AddItem(sideName, LoadTextureOrNull(sideName + "icon.png")); ddPlayerSide.AllowDropDown = false; - ddPlayerSide.SelectedIndexChanged += CopyPlayerDataFromUI; + ddPlayerSide.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); ddPlayerSide.Tag = true; var ddPlayerColor = new XNAClientDropDown(WindowManager); @@ -787,7 +834,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); @@ -798,7 +845,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); @@ -809,7 +856,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; @@ -853,7 +900,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; } @@ -872,24 +919,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) @@ -953,9 +1009,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. @@ -1633,7 +1689,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); @@ -1643,102 +1699,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); + } } /// @@ -1896,25 +1971,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; @@ -2044,7 +2121,7 @@ protected virtual void ChangeMap(GameModeMap gameModeMap) pInfo.TeamId = 1; } - OnGameOptionChanged(); + await OnGameOptionChangedAsync(); MapPreviewBox.GameModeMap = GameModeMap; CopyPlayerDataToUI(); @@ -2288,7 +2365,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) @@ -2313,7 +2390,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 8cbcb997d..fffeb4725 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("UI:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoop(); + AddNotice(string.Format("{0} connected from {1}".L10N("UI: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("UI:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:PlayerLeftGame"), pInfo.Name)); - Players.Remove(pInfo); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - BroadcastPlayerOptions(); - UpdateDiscordPresence(); + AddNotice(string.Format("{0} has left the game.".L10N("UI: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("UI: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("UI: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("UI:Main:MapNotExist") + - "The host needs to change the map or you won't be able to play.".L10N("UI: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("UI:Main:HostChangeFrameSendRate"), frameSendRate)); - } + if (gameModeMap == null) + { + AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("UI:Main:MapNotExist") + + "The host needs to change the map or you won't be able to play.".L10N("UI: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("UI: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("UI:Main:HostEnableOption"), chkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("UI: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("UI:Main:HostEnableOption"), chkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("UI: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("UI:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); + AddNotice(string.Format("The game host has set {0} to {1}".L10N("UI: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 2f6ba8a9e..75084df37 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 Localization; @@ -34,23 +35,25 @@ public MultiplayerGameLobby(WindowManager windowManager, string iniName, chatBoxCommands = new List { - new ChatBoxCommand("HIDEMAPS", "Hide map list (game host only)".L10N("UI:Main:ChatboxCommandHideMapsHelp"), true, + new("HIDEMAPS", "Hide map list (game host only)".L10N("UI:Main:ChatboxCommandHideMapsHelp"), true, s => HideMapList()), - new ChatBoxCommand("SHOWMAPS", "Show map list (game host only)".L10N("UI:Main:ChatboxCommandShowMapsHelp"), true, + new("SHOWMAPS", "Show map list (game host only)".L10N("UI:Main:ChatboxCommandShowMapsHelp"), true, s => ShowMapList()), - new ChatBoxCommand("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("UI:Main:ChatboxCommandFrameSendRateHelp"), true, - s => SetFrameSendRate(s)), - new ChatBoxCommand("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("UI:Main:ChatboxCommandMaxAheadHelp"), true, - s => SetMaxAhead(s)), - new ChatBoxCommand("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("UI:Main:ChatboxCommandProtocolVersionHelp"), true, - s => SetProtocolVersion(s)), - new ChatBoxCommand("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("UI:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), - new ChatBoxCommand("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("UI:Main:ChatboxCommandRandomStartsHelp"), true, - s => SetStartingLocationClearance(s)), - new ChatBoxCommand("ROLL", "Roll dice, for example /roll 3d6".L10N("UI:Main:ChatboxCommandRollHelp"), false, RollDiceCommand), - new ChatBoxCommand("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("UI:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), - new ChatBoxCommand("LOADOPTIONS", "Load game option preset".L10N("UI:Main:ChatboxCommandLoadOptionsHelp"), true, HandleGameOptionPresetLoadCommand) + new("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("UI:Main:ChatboxCommandFrameSendRateHelp"), true, + s => SetFrameSendRateAsync(s)), + new("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("UI:Main:ChatboxCommandMaxAheadHelp"), true, + s => SetMaxAheadAsync(s)), + new("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("UI:Main:ChatboxCommandProtocolVersionHelp"), true, + s => SetProtocolVersionAsync(s)), + new("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("UI:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), + new("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("UI:Main:ChatboxCommandRandomStartsHelp"), true, + s => SetStartingLocationClearanceAsync(s)), + new("ROLL", "Roll dice, for example /roll 3d6".L10N("UI:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType)), + new("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("UI:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), + new("LOADOPTIONS", "Load game option preset".L10N("UI: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. @@ -156,17 +161,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); @@ -197,7 +202,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) @@ -220,7 +225,7 @@ private void FSWEvent(FileSystemEventArgs e) } } - protected override void StartGame() + protected override Task StartGameAsync() { if (fsw != null) fsw.EnableRaisingEvents = true; @@ -228,29 +233,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); } } @@ -274,158 +288,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("UI:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); + if (!IsHost && chatBoxCommand.HostOnly) + { + AddNotice(string.Format("/{0} is for game hosts only.".L10N("UI: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("UI: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("UI:Main:ChatboxCommandTipTitle"), sb.ToString()); + return; } - StringBuilder sb = new StringBuilder("To use a command, start your message with /. Possible chat box commands:".L10N("UI: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("UI: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() { btnLaunchGame.Enabled = !chkAutoReady.Checked; - 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; btnLaunchGame.Enabled = true; } - 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("UI:Main:ChatboxCommandFrameSendRateSyntax")); - return; - } - - FrameSendRate = intValue; - AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("UI:Main:FrameSendRateChanged"), intValue)); + bool success = int.TryParse(value, out int intValue); - OnGameOptionChanged(); - ClearReadyStatuses(); - } + if (!success) + { + AddNotice("Command syntax: /FrameSendRate ".L10N("UI: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("UI:Main:FrameSendRateChanged"), intValue)); - if (!success) + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + ClearReadyStatuses(); + } + catch (Exception ex) { - AddNotice("Command syntax: /MaxAhead ".L10N("UI:Main:ChatboxCommandMaxAheadSyntax")); - return; + PreStartup.HandleException(ex); } - - MaxAhead = intValue; - AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("UI: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("UI:Main:ChatboxCommandMaxAheadSyntax")); + return; + } + + MaxAhead = intValue; + AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("UI:Main:MaxAheadChanged"), intValue)); + + await OnGameOptionChangedAsync(); + ClearReadyStatuses(); + } + catch (Exception ex) { - AddNotice("Command syntax: /ProtocolVersion .".L10N("UI: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("UI:Main:ChatboxCommandProtocolVersionInvalid")); - return; - } + bool success = int.TryParse(value, out int intValue); - ProtocolVersion = intValue; - AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("UI:Main:ProtocolVersionChanged"), intValue)); + if (!success) + { + AddNotice("Command syntax: /ProtocolVersion .".L10N("UI:Main:ChatboxCommandProtocolVersionSyntax")); + return; + } - OnGameOptionChanged(); - ClearReadyStatuses(); + if (!(intValue == 0 || intValue == 2)) + { + AddNotice("ProtocolVersion only allows values 0 and 2.".L10N("UI:Main:ChatboxCommandProtocolVersionInvalid")); + return; + } + + ProtocolVersion = intValue; + AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("UI: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); + } } /// @@ -449,44 +506,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("UI: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("UI:Main:ChatboxCommandRollInvalidAndSyntax")); + return; + } } } - } - if (dieCount > MAX_DICE || dieCount < 1) - { - AddNotice("You can only between 1 to 10 dies at once.".L10N("UI:Main:ChatboxCommandRollInvalid2")); - return; - } + if (dieCount > MAX_DICE || dieCount < 1) + { + AddNotice("You can only between 1 to 10 dies at once.".L10N("UI:Main:ChatboxCommandRollInvalid2")); + return; + } - if (dieSides > MAX_DIE_SIDES || dieSides < 2) - { - AddNotice("You can only have between 2 and 100 sides in a die.".L10N("UI:Main:ChatboxCommandRollInvalid3")); - return; - } + if (dieSides > MAX_DIE_SIDES || dieSides < 2) + { + AddNotice("You can only have between 2 and 100 sides in a die.".L10N("UI: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); } /// @@ -512,7 +576,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. @@ -562,7 +626,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. @@ -701,11 +765,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); + } } /// @@ -714,237 +786,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("UI: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("UI:Main:LockGameNotification")); + protected virtual Task SharedColorsNotificationAsync() + { + try + { + AddNotice("Multiple human players cannot share the same color.".L10N("UI:Main:SharedColorsNotification")); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } - protected virtual void SharedColorsNotification() => - AddNotice("Multiple human players cannot share the same color.".L10N("UI:Main:SharedColorsNotification")); + return Task.CompletedTask; + } - protected virtual void AISpectatorsNotification() => - AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("UI:Main:AISpectatorsNotification")); + protected virtual Task AISpectatorsNotificationAsync() + { + try + { + AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("UI: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("UI: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("UI:Main:NotVerifiedNotification"), Players[playerIndex].Name)); + try + { + AddNotice("Multiple players cannot share the same starting location on this map.".L10N("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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() @@ -969,7 +1146,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); } @@ -1050,13 +1228,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) @@ -1066,21 +1244,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 4421c5eb6..e7136dcfc 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 Localization; @@ -164,29 +165,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("UI:Main:LaunchGameErrorTitle"), error); + } + catch (Exception ex) { - SaveSettings(); - StartGame(); - return; + PreStartup.HandleException(ex); } - - XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("UI: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) @@ -224,13 +241,20 @@ protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.Name, 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 4bf5ac045..432277e23 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("UI:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoop(); + AddNotice(string.Format("{0} connected from {1}".L10N("UI: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("UI:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("UI: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("UI:Main:LoadGameSwitchName"); - } + => "Load Game".L10N("UI: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("UI: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("UI: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 c656bf572..d4cc4a8d9 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("UI: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("UI: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("UI:Main:MainMenu"); - btnMainMenu.LeftClick += BtnMainMenu_LeftClick; + btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(cancellationTokenSource?.Token ?? default); lbGameList = new GameListBox(WindowManager, localGame, null); 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("UI: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("UI:Main:SocketFailure1") + " " + ex.Message)); lbChatMessages.AddMessage(new ChatMessage(Color.Red, "Please check your firewall settings.".L10N("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:NotInSavedGame")); + lbChatMessages.AddMessage( + string.Format("The selected game is for {0}!".L10N("UI: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("UI:Main:NameOccupied")); + lbChatMessages.AddMessage("The selected game is locked!".L10N("UI:Main:GameLocked")); return; } - } - - if (hg.GameVersion != ProgramConstants.GAME_VERSION) - { - // TODO Show warning - } - - lbChatMessages.AddMessage(string.Format("Attempting to join game {0} ...".L10N("UI: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("UI: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("UI: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("UI: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("UI:Main:ConnectGameFailed") + " " + ex.Message, Color.White); } } catch (Exception ex) { - lbChatMessages.AddMessage(null, - "Connecting to the game failed! Message:".L10N("UI: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 18b2852b1..5152ea3e1 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -28,11 +28,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 @@ -46,11 +41,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. /// @@ -69,8 +59,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 0e4773d16..64e1ea886 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 Localization; @@ -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 9fd05d605..23df94c1e 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("UI: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("UI: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("UI: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("UI:Main:NameInUseRetry"), sb.ToString()))); + MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Your name is already in use. Retrying with {0}...".L10N("UI: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 53194e63b..f82cf5b2d 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -2,6 +2,9 @@ using Localization; 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 5707f870e..2a6e3b53e 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -50,9 +50,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); @@ -118,7 +118,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."); TranslationTable.Instance = new TranslationTable(); } @@ -145,7 +145,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 @@ -176,10 +176,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:"); @@ -189,13 +199,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 0ae5ab98b..e578a68e4 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -275,7 +275,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"]; @@ -296,7 +296,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")); } @@ -330,14 +330,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; @@ -351,7 +351,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 18d8b5040ccf1fe07b372ee440b3734f00bee71a Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 00:47:07 +0200 Subject: [PATCH 21/71] 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 fffeb4725..70ffbaa4a 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 75084df37..7fb295b2c 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 d4cc4a8d9..d2531c413 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("UI:Main:MainMenu"); - btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(cancellationTokenSource?.Token ?? default); + btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(); lbGameList = new GameListBox(WindowManager, localGame, null); 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 2a6e3b53e..273fd1c8d 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -196,7 +196,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 84cafe9ca277052a1d0ce965260ff42240a12d32 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 04:17:51 +0200 Subject: [PATCH 22/71] 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 23dadb62a..81b4c3665 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; @@ -981,10 +982,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 d7ea02b7d..e9c4cb05e 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; @@ -1492,7 +1493,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 70ffbaa4a..b6c11392d 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 432277e23..d294bb500 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 d2531c413..081ac09b0 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 6c11499ea7bfef13b1751b0e39f4a801605e8040 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 21 Aug 2022 10:29:52 +0200 Subject: [PATCH 23/71] 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 870b4cb2a..f927412c1 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 964d885cf..c672dba07 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 21e8ad1a3..c21ca2a5c 100644 --- a/DXMainClient/DXGUI/Generic/CampaignSelector.cs +++ b/DXMainClient/DXGUI/Generic/CampaignSelector.cs @@ -326,7 +326,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 2f8caf56f..4fec53653 100644 --- a/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs +++ b/DXMainClient/DXGUI/Generic/GameLoadingWindow.cs @@ -170,7 +170,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 9992cf5b9..f21f2e243 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -301,7 +301,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; @@ -382,7 +382,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) @@ -496,7 +496,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) { @@ -725,7 +725,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 a25b92dab..d4ab5c96c 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 f75578c8d..3a4003ef2 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -614,16 +614,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() @@ -994,14 +1010,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); } } @@ -1104,25 +1127,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 8db590900..d8d0d7458 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 8dc7d96bd..13d051fcd 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("UI: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 81b4c3665..f5534e760 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; public override void Initialize() { @@ -204,7 +205,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; @@ -237,8 +238,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(); } @@ -275,7 +277,7 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI: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) @@ -309,7 +311,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; @@ -317,7 +319,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() { @@ -458,7 +460,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); @@ -485,9 +487,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() { @@ -516,7 +518,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) { @@ -547,7 +549,7 @@ private async Task Channel_UserQuitIRCAsync(UserNameEventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("UI:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); } else { @@ -570,7 +572,7 @@ private async Task Channel_UserLeftAsync(UserNameEventArgs e) { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host abandoned the game.".L10N("UI:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); } else { @@ -585,24 +587,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("UI: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("UI: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); } } @@ -616,7 +625,7 @@ private async Task Channel_UserListReceivedAsync() { connectionManager.MainChannel.AddMessage(new ChatMessage( ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("UI:Main:HostHasAbandoned"))); - await BtnLeaveGame_LeftClickAsync(this, EventArgs.Empty); + await BtnLeaveGame_LeftClickAsync(); } } UpdateDiscordPresence(); @@ -934,40 +943,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); } } @@ -1174,11 +1190,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 e9c4cb05e..860a655d3 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -157,11 +157,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; @@ -169,7 +169,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; } @@ -183,9 +183,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"); @@ -207,10 +204,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"); @@ -265,7 +262,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)); @@ -410,7 +407,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(); @@ -459,17 +456,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) @@ -808,7 +812,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; @@ -823,7 +827,7 @@ protected void InitPlayerOptionDropdowns() foreach (string sideName in sides) ddPlayerSide.AddItem(sideName, LoadTextureOrNull(sideName + "icon.png")); ddPlayerSide.AllowDropDown = false; - ddPlayerSide.SelectedIndexChanged += (sender, e) => CopyPlayerDataFromUIAsync(sender, e); + ddPlayerSide.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); ddPlayerSide.Tag = true; var ddPlayerColor = new XNAClientDropDown(WindowManager); @@ -835,7 +839,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); @@ -846,7 +850,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); @@ -857,7 +861,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; @@ -901,7 +905,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; } @@ -920,7 +924,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 { @@ -1010,9 +1014,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. @@ -1739,7 +1743,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 b6c11392d..e03713eab 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("UI: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 7fb295b2c..db7fbe68c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -374,10 +374,17 @@ private async Task TbChatInput_EnterPressedAsync() } } - private Task ChkAutoReady_CheckedChangedAsync() + private async Task ChkAutoReady_CheckedChangedAsync() { - btnLaunchGame.Enabled = !chkAutoReady.Checked; - return RequestReadyStatusAsync(); + try + { + btnLaunchGame.Enabled = !chkAutoReady.Checked; + await RequestReadyStatusAsync(); + } + catch (Exception ex) + { + PreStartup.HandleException(ex); + } } protected void ResetAutoReadyCheckbox() @@ -785,7 +792,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 { @@ -1091,7 +1098,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 { @@ -1100,7 +1107,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 e7136dcfc..43008043a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -165,7 +165,7 @@ private string CheckGameValidity() return null; } - protected override async Task BtnLaunchGame_LeftClickAsync(object sender, EventArgs e) + protected override async Task BtnLaunchGame_LeftClickAsync() { try { @@ -186,7 +186,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 d294bb500..3bb16f48f 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("UI: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 081ac09b0..033ef5ef1 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 23df94c1e..435c845ef 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 9bdaed85a94fbcec5ec3a3a52d7f36f450a166f6 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 23 Aug 2022 10:04:52 +0200 Subject: [PATCH 24/71] 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 3a4003ef2..8f9718765 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -861,7 +861,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); @@ -1008,7 +1007,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 { @@ -1070,8 +1069,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; } @@ -1079,7 +1077,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, @@ -1603,7 +1600,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 69018dbcc..9492cdb1e 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 34a9ead267a42165abbe2a0a21e9c52d1f880df7 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 26 Aug 2022 19:48:44 +0200 Subject: [PATCH 25/71] 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 50015016e..97b07fa70 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 5152ea3e1..fe650ddc6 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -148,8 +148,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 e578a68e4..33b3bc7f5 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -71,9 +71,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 9cc4b7e862714507e43d458b2b18ca6f3094b609 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 2 Sep 2022 10:28:36 +0200 Subject: [PATCH 26/71] 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 fa8434925bb8c8df8a675159ae2dd68e8fe5876a Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 12 Sep 2022 20:57:27 +0200 Subject: [PATCH 27/71] 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 b68e98cff893f21335d5a3c95eb130a8ff426b51 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Sep 2022 19:53:02 +0200 Subject: [PATCH 28/71] 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 f82cf5b2d..cf1762b1e 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 b715f7fb50a438b0efaa84e3d991be9b99593fcd Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Sep 2022 20:33:29 +0200 Subject: [PATCH 29/71] 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 cf1762b1e..7c3be7fc7 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 da18df3821bca27a4f50f14a3e3d5d6140e1150a Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 14 Sep 2022 00:41:43 +0200 Subject: [PATCH 30/71] 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 3177ede8b..fc1ab7e89 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 1009258b0..29e97e293 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("UI:Main:PrivateMessage"); private readonly string ADD_FRIEND = "Add Friend".L10N("UI: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 d8d0d7458..24d4a407c 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 62b8eb831..3a05195d9 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 13d051fcd..f0d4339f2 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 860a655d3..b931d38e4 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -407,7 +407,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 e03713eab..2e7ea2dec 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 db7fbe68c..43baf8321 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 43008043a..bafc964db 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 64e1ea886..cd7928b8a 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 435c845ef..020263c34 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 7c3be7fc7..a20090253 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 33dd4266ac82672cc27cad3a37bcf111359fd143 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 23 Sep 2022 23:04:55 +0200 Subject: [PATCH 31/71] 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 2e7ea2dec..00ede14d5 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 c37d70f388181b06ef009e7342d0f6d54d2cb18c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 23 Sep 2022 23:08:05 +0200 Subject: [PATCH 32/71] 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 f5534e760..2faba9c4c 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -661,7 +661,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 00ede14d5..342bcfb5d 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 033ef5ef1..f6bce17cc 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 7e7c8e095268c900ab21d71c94cd473da84da7a2 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 23 Sep 2022 23:22:30 +0200 Subject: [PATCH 33/71] 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 342bcfb5d..168b861d7 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 17cfd778b83c3d58f64b3853d5668f621f0e11a7 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 22 Nov 2022 16:03:16 +0100 Subject: [PATCH 34/71] 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 fe650ddc6..21a14750a 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -46,19 +46,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 969d30cfd13eef4b83aebdfd2c30bf8f95363277 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 25 Nov 2022 20:30:29 +0100 Subject: [PATCH 35/71] 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 c21ca2a5c..733151509 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); @@ -307,7 +307,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 4fec53653..91ff82349 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 f0d4339f2..f3d417bae 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 b931d38e4..f7e0308ed 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1297,7 +1297,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 168b861d7..1865a7dc7 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 3bb16f48f..f78723f91 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 f6bce17cc..6b9b68c54 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 8ffbff48e..f275ba1e4 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; @@ -311,4 +309,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 a20090253..79ef57375 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -2,9 +2,7 @@ using Localization; 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 45219f8f1ce1b8e549b2ece470aee7297b5de33b Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 02:00:15 +0100 Subject: [PATCH 36/71] Use SocketsHttpHandler instead of HttpClientHandler --- ClientCore/ClientConfiguration.cs | 6 ++-- ClientCore/Extensions/StringExtensions.cs | 6 ++-- .../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 | 19 +++-------- 9 files changed, 57 insertions(+), 60 deletions(-) diff --git a/ClientCore/ClientConfiguration.cs b/ClientCore/ClientConfiguration.cs index 343169e39..d8a8f38d6 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -202,13 +202,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 0fa5f0d37..2e9c79a63 100644 --- a/ClientCore/Extensions/StringExtensions.cs +++ b/ClientCore/Extensions/StringExtensions.cs @@ -9,11 +9,11 @@ public static string GetLink(this string text) if (string.IsNullOrWhiteSpace(text)) return null; - int index = text.IndexOf("http://", StringComparison.Ordinal); + int index = text.IndexOf($"{Uri.UriSchemeHttp}://", StringComparison.Ordinal); if (index == -1) - index = text.IndexOf("ftp://", StringComparison.Ordinal); + index = text.IndexOf($"{Uri.UriSchemeFtp}://", StringComparison.Ordinal); if (index == -1) - index = text.IndexOf("https://", StringComparison.Ordinal); + 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 e20aece55..cd6cae397 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 Localization; 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 33b3bc7f5..6a51b7ca3 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,33 +24,24 @@ namespace DTAClient /// /// A class that handles initialization of the Client. /// - public class Startup + internal sealed class Startup { /// /// The main method for startup and initialization. /// public void Execute() { - string themePath = ClientConfiguration.Instance.GetThemePath(UserINISettings.Instance.ClientTheme); - - if (themePath == null) - { - themePath = ClientConfiguration.Instance.GetThemeInfoFromIndex(0)[1]; - } - + string themePath = ClientConfiguration.Instance.GetThemePath(UserINISettings.Instance.ClientTheme) + ?? ClientConfiguration.Instance.GetThemeInfoFromIndex(0)[1]; ProgramConstants.RESOURCES_DIR = SafePath.CombineDirectoryPath(ProgramConstants.BASE_RESOURCE_PATH, themePath); - DirectoryInfo resourcesDirectory = SafePath.GetDirectory(ProgramConstants.GetResourcePath()); if (!resourcesDirectory.Exists) 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); @@ -64,8 +54,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 3a9f3257361b67b70d90e7b39eaccb5a096e8339 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 18:12:17 +0100 Subject: [PATCH 37/71] 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 | 54 +- DXMainClient/DXGUI/Generic/TopBar.cs | 15 +- DXMainClient/DXGUI/Generic/UpdateWindow.cs | 7 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 474 +++--- .../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 | 1390 +++++++---------- .../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 | 8 +- 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, 2387 insertions(+), 3627 deletions(-) create mode 100644 DXMainClient/Extensions/TaskExtensions.cs diff --git a/ClientCore/CnCNet5/NameValidator.cs b/ClientCore/CnCNet5/NameValidator.cs index 2c5841ffc..a6d8b9b43 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("UI: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("UI: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 2e9c79a63..2ec52aec2 100644 --- a/ClientCore/Extensions/StringExtensions.cs +++ b/ClientCore/Extensions/StringExtensions.cs @@ -18,7 +18,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 90ed9c112..3d6a9269b 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 c168bbcc0..01817001a 100644 --- a/DXMainClient/DXGUI/GameClass.cs +++ b/DXMainClient/DXGUI/GameClass.cs @@ -169,14 +169,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 f21f2e243..433961c18 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -38,13 +38,13 @@ class MainMenu : XNAWindow, ISwitchable /// Creates a new instance of the main menu. /// public MainMenu( - WindowManager windowManager, + WindowManager windowManager, SkirmishLobby skirmishLobby, - LANLobby lanLobby, - TopBar topBar, + LANLobby lanLobby, + TopBar topBar, OptionsWindow optionsWindow, CnCNetLobby cncnetLobby, - CnCNetManager connectionManager, + CnCNetManager connectionManager, DiscordHandler discordHandler, CnCNetGameLoadingLobby cnCNetGameLoadingLobby, CnCNetGameLobby cnCNetGameLobby, @@ -182,7 +182,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); @@ -301,7 +301,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; @@ -496,8 +496,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) @@ -531,22 +529,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 fc1ab7e89..8ec61cebb 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("UI:Main:LogOut"); 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 78bce3447..400c653ef 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 d4ab5c96c..85bcd13ef 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("UI:Main:PlayerCheating"), cheaterName), Color.Red); + AddNotice(string.Format("{0} - modified files detected! They could be cheating!".L10N("UI: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("UI: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("UI: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,44 +592,37 @@ private void HandleTunnelServerChange(CnCNetTunnel tunnel) protected override async Task HostStartGameAsync() { - try + AddNotice("Contacting tunnel server...".L10N("UI:Main:ConnectingTunnel")); + List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(SGPlayers.Count); + + if (playerPorts.Count < Players.Count) { - AddNotice("Contacting tunnel server...".L10N("UI: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." + Environment.NewLine + - "Try picking a different tunnel server:").L10N("UI:Main:ConnectTunnelError1")); - AddNotice(("An error occured while contacting the specified CnCNet " + - "tunnel server. Please try using a different tunnel server ").L10N("UI: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("UI:Main:StartingGame")); - - started = true; - - LoadGame(); + ShowTunnelSelectionWindow(("An error occured while contacting " + + "the CnCNet tunnel server." + Environment.NewLine + + "Try picking a different tunnel server:").L10N("UI:Main:ConnectTunnelError1")); + AddNotice(("An error occured while contacting the specified CnCNet " + + "tunnel server. Please try using a different tunnel server ").L10N("UI: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("UI:Main:StartingGame")); + + started = true; + + LoadGame(); } protected override void WriteSpawnIniAdditions(IniFile spawnIni) @@ -736,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(); @@ -758,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(lblMapNameValue.Text); - sb.Append(";"); - sb.Append(lblGameModeValue.Text); - 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(lblMapNameValue.Text); + sb.Append(";"); + sb.Append(lblGameModeValue.Text); + 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("UI:Main:LoadGame"); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 8f9718765..f946bf195 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -51,7 +51,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) }; @@ -172,19 +172,18 @@ public override void Initialize() btnNewGame.Y, UIDesignConstants.BUTTON_WIDTH_133, UIDesignConstants.BUTTON_HEIGHT); btnJoinGame.Text = "Join Game".L10N("UI: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("UI: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.ClientRectangle = gameListRectangle; @@ -195,7 +194,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; @@ -211,7 +210,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); @@ -231,7 +230,7 @@ public override void Initialize() tbChatInput.Suggestion = "Type here to chat...".L10N("UI: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); @@ -270,7 +269,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); @@ -296,8 +295,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("UI:Main:FilterByBlahBlah"); tbGameSearch.MaximumTextLength = 64; tbGameSearch.InputReceived += TbGameSearch_InputReceived; @@ -355,15 +353,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(); @@ -524,7 +522,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; @@ -538,8 +536,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(); @@ -547,7 +545,7 @@ private void PostUIInit() string.Format("*** DTA CnCNet Client version {0} ***".L10N("UI: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; @@ -561,7 +559,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; @@ -572,10 +570,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(); } /// @@ -584,96 +581,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("UI: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("UI:Main:PlayerBannedByHost"), game.RoomName))); - - 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("UI: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("UI: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); } } @@ -778,12 +742,6 @@ private void SetLogOutButtonText() btnLogout.Text = "Log Out".L10N("UI: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) @@ -829,18 +787,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) @@ -894,95 +845,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("UI:Main:AttemptJoin"), hg.RoomName))); - isJoiningGame = true; - gameOfLastJoinAttempt = hg; + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, + string.Format("Attempting to join game {0} ...".L10N("UI: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("UI: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("UI: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) @@ -996,34 +922,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("UI:Main:PasswordWrong"))); - await ClearGameJoinAttemptAsync((Channel)sender); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("UI: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(); } } @@ -1059,104 +971,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("UI: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("UI: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("UI: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("UI: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; } } @@ -1179,21 +1063,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() @@ -1238,45 +1115,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) @@ -1292,107 +1162,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("UI:Main:GameInviteTitle"), - GetUserTexture(sender), - sender, - string.Format("Join {0}?".L10N("UI:Main:GameInviteText"), gameName), - "Yes".L10N("UI:Main:ButtonYes"), "No".L10N("UI: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("UI:Main:GameInviteTitle"), + GetUserTexture(sender), + sender, + string.Format("Join {0}?".L10N("UI:Main:GameInviteText"), gameName), + "Yes".L10N("UI:Main:ButtonYes"), "No".L10N("UI: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("UI:Main:JoinFailedTitle"), - string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("UI: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("UI:Main:JoinFailedTitle"), + string.Format("Unable to join {0}'s game. The game may be locked or closed.".L10N("UI:Main:JoinFailedText"), sender)); } + + // clean up the index as this invitation no longer exists + invitationIndex.Remove(invitationIdentity); } private void HandleGameInvitationFailedNotification(string sender) @@ -1411,58 +1265,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(); } } @@ -1563,7 +1410,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("UI:Main:UpdateAvailableTitle"), @@ -1576,7 +1423,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) @@ -1594,7 +1441,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); @@ -1680,26 +1527,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() @@ -1804,27 +1644,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("UI: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("UI: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("UI: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("UI: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 3044e1b18..3cc165413 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 29e97e293..1f122dbe4 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 24d4a407c..fa8842e0a 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 f3d417bae..8378187ec 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("UI: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("UI: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; } @@ -500,40 +478,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 2faba9c4c..f3814f443 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("UI:Main:TunnelInfo"), 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("UI:Main:TunnelInfo"), + false, + PrintTunnelServerInformation)); + AddChatBoxCommand(new ChatBoxCommand( + "CHANGETUNNEL", "Change the used CnCNet tunnel server (game host only)".L10N("UI:Main:ChangeTunnel"), - true, (s) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer")))); - AddChatBoxCommand(new ChatBoxCommand("DOWNLOADMAP", + true, + _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer")))); + 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("UI:Main:DownloadMapCommandDescription"), - false, DownloadMapByIdCommand)); + false, + DownloadMapByIdCommand)); } public event EventHandler GameLeft; @@ -195,8 +203,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; @@ -205,7 +213,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; @@ -219,7 +227,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); @@ -233,14 +241,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(); } @@ -277,8 +285,6 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI: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) { @@ -319,8 +325,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(); @@ -359,23 +363,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); } } @@ -409,16 +406,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) @@ -470,39 +462,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) @@ -518,7 +491,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) { @@ -541,146 +515,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("UI: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("UI: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("UI: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("UI: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("UI:Main:YouWereKicked"))); - await ClearAsync(); - Visible = false; - Enabled = false; - return; - } + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("UI: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("UI:Main:HostHasAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); - } + connectionManager.MainChannel.AddMessage(new ChatMessage( + ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("UI: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("UI: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("UI:Main:GameRoomNumberLimitReached")); + await LockGameAsync(); } } @@ -761,44 +701,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("UI:Main:ConnectingTunnel")); + AddNotice("Contacting tunnel server...".L10N("UI: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; + } + + 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); - await StartGameAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + await StartGameAsync(); } private async Task StartGame_V2TunnelAsync() @@ -896,40 +829,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) @@ -942,47 +851,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(); } } @@ -1052,71 +954,64 @@ protected override async Task RequestReadyStatusAsync() /// private async Task HandleOptionsRequestAsync(string playerName, int options) { - try - { - if (!IsHost) - return; - - if (ProgramConstants.IsInGame) - return; + if (!IsHost) + return; - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + if (ProgramConstants.IsInGame) + return; - if (pInfo == null) - return; + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - byte[] bytes = BitConverter.GetBytes(options); + if (pInfo == null) + return; - int side = bytes[0]; - int color = bytes[1]; - int start = bytes[2]; - int team = bytes[3]; + byte[] bytes = BitConverter.GetBytes(options); - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + int side = bytes[0]; + int color = bytes[1]; + int start = bytes[2]; + int team = bytes[3]; - if (color < 0 || color > MPColors.Count) - return; - - 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(); } /// @@ -1124,27 +1019,19 @@ private async Task HandleOptionsRequestAsync(string playerName, int options) /// private async Task HandleReadyRequestAsync(string playerName, int readyStatus) { - try - { - if (!IsHost) - return; - - PlayerInfo pInfo = Players.Find(p => p.Name == playerName); + if (!IsHost) + return; - if (pInfo == null) - return; + PlayerInfo pInfo = Players.Find(p => p.Name == playerName); - pInfo.Ready = readyStatus > 0; - pInfo.AutoReady = readyStatus > 1; + if (pInfo == null) + return; - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + pInfo.Ready = readyStatus > 0; + pInfo.AutoReady = readyStatus > 1; - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + CopyPlayerDataToUI(); + await BroadcastPlayerOptionsAsync(); } /// @@ -1191,17 +1078,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() @@ -1368,175 +1246,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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:HostEnableOption"), checkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("UI: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("UI:Main:HostEnableOption"), checkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("UI: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("UI:Main:HostGameOptionInvalid"), Color.Red); - return; - } + AddNotice(("The game host has sent an invalid game options message! " + + "The game host's game version might be different from yours.").L10N("UI: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("UI:Main:HostGameOptionInvalidTheSecondTime"), 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("UI: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; - - AddNotice(string.Format("The game host has set {0} to {1}".L10N("UI: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("UI: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("UI: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("UI:Main:HostRandomSeedError"), Color.Red); } + + bool removeStartingLocations = Convert.ToBoolean(Conversions.IntFromString(parts[partIndex + 7], + Convert.ToInt32(RemoveStartingLocations))); + SetRandomStartingLocations(removeStartingLocations); + + RandomSeed = randomSeed; } private async Task RequestMapAsync() @@ -1583,29 +1454,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); - - if (IsHost) - { - RandomSeed = new Random().Next(); - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); - await BroadcastPlayerExtraOptionsAsync(); + await base.GameProcessExitedAsync(); + await channel.SendCTCPMessageAsync("RETURN", QueuedMessageType.SYSTEM_MESSAGE, 20); + ReturnNotification(ProgramConstants.PLAYERNAME); - 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); } } @@ -1614,59 +1477,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() @@ -1731,141 +1586,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) @@ -1893,28 +1685,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); } } @@ -2010,33 +1794,26 @@ private void HandleCheatDetectedMessage(string sender) => private async Task HandleTunnelServerChangeMessageAsync(string sender, string tunnelAddressAndPort) { - try - { - if (sender != hostName) - return; + 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) - { - 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("UI:Main:HostInvalidTunnel"), - Color.Yellow); - btnLaunchGame.AllowClick = false; - return; - } - - await HandleTunnelServerChangeAsync(tunnel); - btnLaunchGame.AllowClick = true; - } - catch (Exception ex) + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Find(t => t.Address == tunnelAddress && t.Port == tunnelPort); + if (tunnel == null) { - PreStartup.HandleException(ex); + 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("UI:Main:HostInvalidTunnel"), + Color.Yellow); + btnLaunchGame.AllowClick = false; + return; } + + await HandleTunnelServerChangeAsync(tunnel); + btnLaunchGame.AllowClick = true; } /// @@ -2052,126 +1829,86 @@ private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) #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("UI: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("UI: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("UI:Main:DownloadMapCommandFailedGeneric")); - mapSharingConfirmationPanel.SetFailedStatus(); - return; - } - - AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("UI: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("UI: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("UI: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) { - 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("UI: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("UI: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)) { - PreStartup.HandleException(ex); + // 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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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); } } @@ -2315,8 +2052,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(); } @@ -2365,61 +2102,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.Name); - sb.Append(";"); - sb.Append(GameMode.UIName); - 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.Name); + sb.Append(";"); + sb.Append(GameMode.UIName); + 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 f7e0308ed..fd5a51644 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -204,10 +204,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"); @@ -238,8 +238,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; @@ -248,7 +247,7 @@ public override void Initialize() lbGameModeMapList.AddColumn("MAP NAME".L10N("UI: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) @@ -262,12 +261,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(); } @@ -286,8 +285,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("UI:Main:MapSortAlphabeticallyToolTip")); RefreshMapSortAlphabeticallyBtn(); @@ -386,63 +384,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("UI:Main:PresetLoaded")); - else - AddNotice(string.Format("Preset {0} not found!".L10N("UI:Main:PresetNotFound"), presetName)); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + if (await LoadGameOptionPresetAsync(presetName)) + AddNotice("Game option preset loaded succesfully.".L10N("UI:Main:PresetLoaded")); + else + AddNotice(string.Format("Preset {0} not found!".L10N("UI: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() @@ -456,27 +431,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(); @@ -632,7 +600,7 @@ private void DeleteMapConfirmation() var messageBox = XNAMessageBox.ShowYesNoDialog(WindowManager, "Delete Confirmation".L10N("UI:Main:DeleteMapConfirmTitle"), string.Format("Are you sure you wish to delete the custom map {0}?".L10N("UI:Main:DeleteMapConfirmText"), Map.Name)); - messageBox.YesClickedAction = _ => DeleteSelectedMapAsync(); + messageBox.YesClickedAction = _ => DeleteSelectedMapAsync().HandleTask(); } private void MapPreviewBox_ToggleFavorite(object sender, EventArgs e) => @@ -684,59 +652,40 @@ private async Task DeleteSelectedMapAsync() XNAMessageBox.Show(WindowManager, "Deleting Map Failed".L10N("UI:Main:DeleteMapFailedTitle"), "Deleting map failed! Reason:".L10N("UI: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 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) @@ -789,9 +738,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(','); SideCount = sides.Length; @@ -812,7 +758,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; @@ -827,7 +773,7 @@ protected void InitPlayerOptionDropdowns() foreach (string sideName in sides) ddPlayerSide.AddItem(sideName, LoadTextureOrNull(sideName + "icon.png")); ddPlayerSide.AllowDropDown = false; - ddPlayerSide.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender); + ddPlayerSide.SelectedIndexChanged += (sender, _) => CopyPlayerDataFromUIAsync(sender).HandleTask(); ddPlayerSide.Tag = true; var ddPlayerColor = new XNAClientDropDown(WindowManager); @@ -839,7 +785,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); @@ -850,7 +796,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); @@ -861,7 +807,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; @@ -905,7 +851,7 @@ protected void InitPlayerOptionDropdowns() { PlayerExtraOptionsPanel = FindChild(nameof(PlayerExtraOptionsPanel)); PlayerExtraOptionsPanel.Disable(); - PlayerExtraOptionsPanel.OptionsChanged += (_, _) => PlayerExtraOptions_OptionsChangedAsync(); + PlayerExtraOptionsPanel.OptionsChanged += (_, _) => PlayerExtraOptions_OptionsChangedAsync().HandleTask(); btnPlayerExtraOptionsOpen.LeftClick += BtnPlayerExtraOptions_LeftClick; } @@ -926,29 +872,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; } @@ -1708,33 +1647,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; } @@ -1745,80 +1670,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 1865a7dc7..dedc7d72e 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("UI:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoopAsync(cancellationToken); + AddNotice(string.Format("{0} connected from {1}".L10N("UI: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("UI:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:MapNotExist") + - "The host needs to change the map or you won't be able to play.".L10N("UI: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("UI:Main:MapNotExist") + + "The host needs to change the map or you won't be able to play.".L10N("UI: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("UI: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("UI: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("UI:Main:HostEnableOption"), chkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("UI:Main:HostDisableOption"), chkBox.Text)); - } + if (chkBox.Checked != oldValue) + { + if (chkBox.Checked) + AddNotice(string.Format("The game host has enabled {0}".L10N("UI:Main:HostEnableOption"), chkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("UI: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("UI:Main:HostSetOption"), ddName, dd.SelectedItem.Text)); - } + AddNotice(string.Format("The game host has set {0} to {1}".L10N("UI: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 2bc046371..a9ca89dc6 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 43baf8321..e9d49267b 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("UI:Main:ChatboxCommandShowMapsHelp"), true, s => ShowMapList()), new("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("UI:Main:ChatboxCommandFrameSendRateHelp"), true, - s => SetFrameSendRateAsync(s)), + s => SetFrameSendRateAsync(s).HandleTask()), new("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("UI:Main:ChatboxCommandMaxAheadHelp"), true, - s => SetMaxAheadAsync(s)), + s => SetMaxAheadAsync(s).HandleTask()), new("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("UI:Main:ChatboxCommandProtocolVersionHelp"), true, - s => SetProtocolVersionAsync(s)), + s => SetProtocolVersionAsync(s).HandleTask()), new("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("UI:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), new("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("UI:Main:ChatboxCommandRandomStartsHelp"), true, - s => SetStartingLocationClearanceAsync(s)), - new("ROLL", "Roll dice, for example /roll 3d6".L10N("UI:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType)), + s => SetStartingLocationClearanceAsync(s).HandleTask()), + new("ROLL", "Roll dice, for example /roll 3d6".L10N("UI:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType).HandleTask()), new("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("UI:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), - new("LOADOPTIONS", "Load game option preset".L10N("UI:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName)) + new("LOADOPTIONS", "Load game option preset".L10N("UI:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName).HandleTask()) }; - chkAutoReady_CheckedChangedFunc = (_, _) => ChkAutoReady_CheckedChangedAsync(); + chkAutoReady_CheckedChangedFunc = (_, _) => ChkAutoReady_CheckedChangedAsync().HandleTask(); } protected XNAPlayerSlotIndicator[] StatusIndicators; @@ -160,17 +164,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); @@ -237,33 +241,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(); } } @@ -287,18 +283,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) @@ -313,78 +297,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("UI:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); - return; - } - - chatBoxCommand.Action(parameters); + AddNotice(string.Format("/{0} is for game hosts only.".L10N("UI:Main:ChatboxCommandHostOnly"), chatBoxCommand.Command)); return; } - } - StringBuilder sb = new StringBuilder("To use a command, start your message with /. Possible chat box commands:".L10N("UI: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("UI: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("UI: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("UI:Main:ChatboxCommandTipTitle"), sb.ToString()); + return; } + + await SendChatMessageAsync(tbChatInput.Text); + tbChatInput.Text = string.Empty; } private async Task ChkAutoReady_CheckedChangedAsync() { - try - { - btnLaunchGame.Enabled = !chkAutoReady.Checked; - await RequestReadyStatusAsync(); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + btnLaunchGame.Enabled = !chkAutoReady.Checked; + await RequestReadyStatusAsync(); } protected void ResetAutoReadyCheckbox() @@ -397,98 +367,70 @@ protected void ResetAutoReadyCheckbox() private async Task SetFrameSendRateAsync(string value) { - try - { - bool success = int.TryParse(value, out int intValue); - - if (!success) - { - AddNotice("Command syntax: /FrameSendRate ".L10N("UI:Main:ChatboxCommandFrameSendRateSyntax")); - return; - } + bool success = int.TryParse(value, out int intValue); - FrameSendRate = intValue; - AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("UI:Main:FrameSendRateChanged"), intValue)); - - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - ClearReadyStatuses(); - } - catch (Exception ex) + if (!success) { - PreStartup.HandleException(ex); + AddNotice("Command syntax: /FrameSendRate ".L10N("UI:Main:ChatboxCommandFrameSendRateSyntax")); + return; } + + FrameSendRate = intValue; + AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("UI: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("UI:Main:ChatboxCommandMaxAheadSyntax")); - return; - } + bool success = int.TryParse(value, out int intValue); - MaxAhead = intValue; - AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("UI:Main:MaxAheadChanged"), intValue)); - - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); - } - catch (Exception ex) + if (!success) { - PreStartup.HandleException(ex); + AddNotice("Command syntax: /MaxAhead ".L10N("UI:Main:ChatboxCommandMaxAheadSyntax")); + return; } + + MaxAhead = intValue; + AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("UI: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("UI: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("UI:Main:ChatboxCommandProtocolVersionInvalid")); - return; - } - - ProtocolVersion = intValue; - AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("UI:Main:ProtocolVersionChanged"), intValue)); - - await OnGameOptionChangedAsync(); - ClearReadyStatuses(); + if (!success) + { + AddNotice("Command syntax: /ProtocolVersion .".L10N("UI:Main:ChatboxCommandProtocolVersionSyntax")); + return; } - catch (Exception ex) + + if (!(intValue == 0 || intValue == 2)) { - PreStartup.HandleException(ex); + AddNotice("ProtocolVersion only allows values 0 and 2.".L10N("UI:Main:ChatboxCommandProtocolVersionInvalid")); + return; } + + ProtocolVersion = intValue; + AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("UI: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(); } /// @@ -514,49 +456,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("UI:Main:ChatboxCommandRollInvalidAndSyntax")); - return; - } + AddNotice("Invalid dice specified. Expected format: /roll d".L10N("UI:Main:ChatboxCommandRollInvalidAndSyntax")); + return; } } + } - if (dieCount > MAX_DICE || dieCount < 1) - { - AddNotice("You can only between 1 to 10 dies at once.".L10N("UI:Main:ChatboxCommandRollInvalid2")); - return; - } - - if (dieSides > MAX_DIE_SIDES || dieSides < 2) - { - AddNotice("You can only have between 2 and 100 sides in a die.".L10N("UI: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("UI: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("UI: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); } /// @@ -773,17 +708,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(); } /// @@ -794,234 +721,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)) + 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) { - 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("UI:Main:LockGameNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("You need to lock the game room before launching the game.".L10N("UI:Main:LockGameNotification")); return Task.CompletedTask; } protected virtual Task SharedColorsNotificationAsync() { - try - { - AddNotice("Multiple human players cannot share the same color.".L10N("UI:Main:SharedColorsNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("Multiple human players cannot share the same color.".L10N("UI:Main:SharedColorsNotification")); return Task.CompletedTask; } protected virtual Task AISpectatorsNotificationAsync() { - try - { - AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("UI:Main:AISpectatorsNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("AI players don't enjoy spectating matches. They want some action!".L10N("UI:Main:AISpectatorsNotification")); return Task.CompletedTask; } protected virtual Task SharedStartingLocationNotificationAsync() { - try - { - AddNotice("Multiple players cannot share the same starting location on this map.".L10N("UI:Main:SharedStartingLocationNotification")); - } - catch (Exception ex) - { - PreStartup.HandleException(ex); - } + AddNotice("Multiple players cannot share the same starting location on this map.".L10N("UI: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("UI: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("UI: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("UI: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("UI:Main:StillInGameNotification"), + Players[playerIndex].Name)); } return Task.CompletedTask; @@ -1029,51 +905,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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:TooManyPlayersNotification"), + Map.MaxPlayers)); return Task.CompletedTask; } @@ -1100,34 +955,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() @@ -1254,7 +1102,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 bafc964db..f33efbd30 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -167,42 +167,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("UI:Main:LaunchGameErrorTitle"), error); - } - catch (Exception ex) + if (error == null) { - PreStartup.HandleException(ex); + SaveSettings(); + await StartGameAsync(); + return; } + + XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("UI: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; } @@ -243,18 +229,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 f78723f91..970ea2ba9 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("UI:Main:PlayerFromIP"), lpInfo.Name, lpInfo.IPAddress)); - lpInfo.StartReceiveLoopAsync(cancellationToken); + AddNotice(string.Format("{0} connected from {1}".L10N("UI: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("UI:Main:PlayerLeftGame"), lpInfo.Name)); + AddNotice(string.Format("{0} has left the game.".L10N("UI: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 6b9b68c54..96a380131 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("UI: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("UI: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("UI:Main:MainMenu"); - btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync(); + btnMainMenu.LeftClick += (_, _) => BtnMainMenu_LeftClickAsync().HandleTask(); lbGameList = new GameListBox(WindowManager, localGame, null); 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("UI: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("UI: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("UI: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("UI:Main:GameIsOfPurpose"), gameCollection.GetGameNameFromInternalName(hg.Game.InternalName))); + lbChatMessages.AddMessage("You do not exist in the saved game!".L10N("UI:Main:NotInSavedGame")); return; } - - if (hg.Locked) + } + else + { + if (hg.Players.Contains(ProgramConstants.PLAYERNAME)) { - lbChatMessages.AddMessage("The selected game is locked!".L10N("UI:Main:GameLocked")); + lbChatMessages.AddMessage("Your name is already taken in the game.".L10N("UI:Main:NameOccupied")); return; } + } - if (hg.IsLoadedGame) - { - if (!hg.Players.Contains(ProgramConstants.PLAYERNAME)) - { - lbChatMessages.AddMessage("You do not exist in the saved game!".L10N("UI:Main:NotInSavedGame")); - return; - } - } - else - { - if (hg.Players.Contains(ProgramConstants.PLAYERNAME)) - { - lbChatMessages.AddMessage("Your name is already taken in the game.".L10N("UI: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("UI:Main:AttemptJoin"), hg.RoomName)); + lbChatMessages.AddMessage(string.Format("Attempting to join game {0} ...".L10N("UI: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("UI: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("UI: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 beea07a66..046b1a158 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -525,7 +525,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; @@ -546,7 +546,7 @@ public bool SetInfoFromCustomMap() HumanPlayersOnly = basicSection.GetBooleanValue("HumanPlayersOnly", false); ForceRandomStartLocations = basicSection.GetBooleanValue("ForceRandomStartLocations", false); ForceNoTeams = basicSection.GetBooleanValue("ForceNoTeams", false); - PreviewPath = Path.ChangeExtension(customMapFilePath.Substring(ProgramConstants.GamePath.Length), ".png"); + PreviewPath = Path.ChangeExtension(customMapFilePath[ProgramConstants.GamePath.Length..], ".png"); MultiplayerOnly = basicSection.GetBooleanValue("ClientMultiplayerOnly", false); string bases = basicSection.GetStringValue("Bases", string.Empty); @@ -846,8 +846,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 21a14750a..7b5d7dcee 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -44,28 +44,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) @@ -137,7 +130,7 @@ private void LoadGameModeAliases(IniFile mpMapsIni) } } - private void LoadCustomMaps() + private async Task LoadCustomMapsAsync() { DirectoryInfo customMapsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY); @@ -148,7 +141,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(); @@ -157,8 +150,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]; Map map = new Map(baseFilePath .Replace(Path.DirectorySeparatorChar, '/') @@ -170,7 +163,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))) @@ -207,14 +200,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 020263c34..4aa515be1 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("UI: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("UI: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("UI: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("UI: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 79ef57375..aadabf0e9 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 273fd1c8d..30e2e418f 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -216,7 +216,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 6a51b7ca3..abca810c4 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -57,7 +57,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 2d2d8b5d99c3f236c09b490b617ce208edb85151 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 18:50:18 +0100 Subject: [PATCH 38/71] 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 dedc7d72e..43418ae59 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 970ea2ba9..8cb534a7e 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 96a380131..f2c986935 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 c4b63018824b78d3d2def23b376363a9be7455f8 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 18:51:47 +0100 Subject: [PATCH 39/71] 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 0b89cc58e3820d4cf2b527fc1eeb52c4955305cb Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 19:18:29 +0100 Subject: [PATCH 40/71] 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 d3c1328f2..5c698d69b 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -39,7 +39,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"; public const int GAME_ID_MAX_LENGTH = 4; 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 91ff82349..917a60bb1 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; @@ -164,7 +162,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(); } @@ -185,7 +183,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 f946bf195..d32ef3163 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -849,7 +849,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 8378187ec..19d2dc51b 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("UI:Main:ButtonLoadGame") : "I'm Ready".L10N("UI: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.Text = spawnSGIni.GetStringValue("Settings", "UIMapName", string.Empty); lblGameModeValue.Text = spawnSGIni.GetStringValue("Settings", "UIGameMode", string.Empty); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs index e9d49267b..b4ef974d5 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -184,7 +184,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 abca810c4..fe5eecc9d 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -80,7 +80,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 32cf9efb1d86ecf428fc86b18c5806671087e65c Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 26 Nov 2022 21:47:49 +0100 Subject: [PATCH 41/71] Exception logging --- ClientCore/ClientConfiguration.cs | 3 +- .../Extensions/TaskExtensions.cs | 26 ++++- .../PreprocessorBackgroundTask.cs | 3 +- ClientCore/ProgramConstants.cs | 85 +++++++++++++- 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 | 4 +- .../Multiplayer/CnCNet/GlobalContextMenu.cs | 3 +- .../CnCNet/PrivateMessagingWindow.cs | 1 + .../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 | 104 ++++-------------- DXMainClient/Startup.cs | 60 +++++----- 49 files changed, 369 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 d8a8f38d6..4be547229 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -339,7 +339,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 5c698d69b..a9ea75dd7 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -24,6 +24,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; @@ -41,12 +47,15 @@ public static class ProgramConstants public const string SPAWNER_SETTINGS = "spawn.ini"; public const string SAVED_GAME_SPAWN_INI = SAVED_GAMES_DIRECTORY + "/spawnSG.ini"; public const string SAVED_GAMES_DIRECTORY = "Saved Games"; - + 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 @@ -107,5 +116,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("UI:Main:FatalErrorText1") + Environment.NewLine + Environment.NewLine + + ex.Message + Environment.NewLine + Environment.NewLine + (crashLogCopied ? + "A crash log has been saved to the following file:".L10N("UI: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("UI:Main:FatalErrorText3") : + "If the issue is repeatable, contact the {1} staff at {2}.".L10N("UI:Main:FatalErrorText4")), + GAME_NAME_LONG, + GAME_NAME_SHORT, + SUPPORT_URL_SHORT); + + DisplayErrorAction("KABOOOOOOOM".L10N("UI: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 c74fcaafa..2786f0d5d 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -93,7 +93,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); @@ -124,7 +124,7 @@ public static void StartGameProcess(WindowManager windowManager) } catch (Exception ex) { - Logger.Log("Error launching " + gameExecutableName + ": " + ex.Message); + ProgramConstants.LogException(ex, "Error launching " + gameExecutableName); XNAMessageBox.Show(windowManager, "Error launching game", "Error launching " + gameExecutableName + ". 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 d7dc4ffe2..6ef778a2f 100644 --- a/DTAConfig/OptionPanels/DisplayOptionsPanel.cs +++ b/DTAConfig/OptionPanels/DisplayOptionsPanel.cs @@ -415,10 +415,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")] @@ -452,7 +455,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("UI:DTAConfig:TSFixUninstallFailTitle"), "Uninstalling DTA/TI/TS Compatibility Fix failed. Returned error:".L10N("UI:DTAConfig:TSFixUninstallFailText") + " " + ex.Message); } @@ -480,7 +483,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("UI:DTAConfig:TSFixInstallFailTitle"), "Installing DTA/TI/TS Compatibility Fix failed. Error message:".L10N("UI:DTAConfig:TSFixInstallFailText") + " " + ex.Message); } @@ -511,7 +514,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("UI:DTAConfig:TSFinalSunFixUninstallFailedTitle"), "Uninstalling FinalSun Compatibility Fix failed. Error message:".L10N("UI:DTAConfig:TSFinalSunFixUninstallFailedText") + " " + ex.Message); } @@ -539,7 +542,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("UI:DTAConfig:TSFinalSunCompatibilityFixInstalledFailedTitle"), "Installing FinalSun Compatibility Fix failed. Error message:".L10N("UI:DTAConfig:TSFinalSunCompatibilityFixInstalledFailedText") + " " + ex.Message); } diff --git a/DTAConfig/OptionsWindow.cs b/DTAConfig/OptionsWindow.cs index ecee155c3..78fa4dffa 100644 --- a/DTAConfig/OptionsWindow.cs +++ b/DTAConfig/OptionsWindow.cs @@ -193,7 +193,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("UI:DTAConfig:SaveSettingFailTitle"), "Saving settings failed! Error message:".L10N("UI:DTAConfig:SaveSettingFailText") + " " + ex.Message); } diff --git a/DXMainClient/DXGUI/GameClass.cs b/DXMainClient/DXGUI/GameClass.cs index 01817001a..0dabf7edb 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(); @@ -84,41 +84,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 110b1cc98..15cd60fb7 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 97b07fa70..46f8810b6 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 433961c18..f75cf9fb0 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; namespace DTAClient.DXGUI.Generic @@ -374,10 +375,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!"); } } @@ -643,7 +641,7 @@ private void UpdateWindow_UpdateFailed(object sender, UpdateFailureEventArgs e) "If you are connected to the Internet and your firewall isn't blocking" + Environment.NewLine + "{1}, and the issue is reproducible, contact us at " + Environment.NewLine + "{2} for support.").L10N("UI: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(); } @@ -666,7 +664,7 @@ private void UpdateWindow_UpdateCompleted(object sender, EventArgs e) { innerPanel.Hide(); lblUpdateStatus.Text = string.Format("{0} was succesfully updated to v.{1}".L10N("UI:Main:UpdateSuccess"), - MainClientConstants.GAME_NAME_SHORT, Updater.GameVersion); + ProgramConstants.GAME_NAME_SHORT, Updater.GameVersion); lblVersion.Text = Updater.GameVersion; UpdateInProgress = false; lblUpdateStatus.Enabled = true; @@ -730,7 +728,7 @@ private void HandleFileIdentifierUpdate() if (Updater.VersionState == VersionState.UPTODATE) { - lblUpdateStatus.Text = string.Format("{0} is up to date.".L10N("UI:Main:GameUpToDate"), MainClientConstants.GAME_NAME_SHORT); + lblUpdateStatus.Text = string.Format("{0} is up to date.".L10N("UI: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!"); } } } @@ -1034,7 +1032,7 @@ private void MusicOff() } catch (Exception ex) { - Logger.Log("Turning music off failed! Message: " + ex.Message); + ProgramConstants.LogException(ex, "Turning music off failed!"); } } @@ -1050,9 +1048,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 8ec61cebb..7c5384174 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 400c653ef..ad46464b8 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 } @@ -291,14 +293,14 @@ public void SetData(string newGameVersion) { lblDescription.Text = string.Format(("Please wait while {0} is updated to version {1}." + Environment.NewLine + "This window will automatically close once the update is complete." + Environment.NewLine + Environment.NewLine + - "The client may also restart after the update has been downloaded.").L10N("UI:Main:UpdateVersionPleaseWait"), MainClientConstants.GAME_NAME_SHORT, newGameVersion); + "The client may also restart after the update has been downloaded.").L10N("UI:Main:UpdateVersionPleaseWait"), ProgramConstants.GAME_NAME_SHORT, newGameVersion); lblUpdaterStatus.Text = "Preparing".L10N("UI:Main:StatusPreparing"); } public void ForceUpdate() { isStartingForceUpdate = true; - lblDescription.Text = string.Format("Force updating {0} to latest version...".L10N("UI:Main:ForceUpdateToLatest"), MainClientConstants.GAME_NAME_SHORT); + lblDescription.Text = string.Format("Force updating {0} to latest version...".L10N("UI:Main:ForceUpdateToLatest"), ProgramConstants.GAME_NAME_SHORT); lblUpdaterStatus.Text = "Connecting".L10N("UI:Main:UpdateStatusConnecting"); Updater.CheckForUpdates(); } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetGameLoadingLobby.cs index 85bcd13ef..c579d5570 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 d32ef3163..151e0631f 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -20,6 +20,7 @@ using System.Threading; using System.Threading.Tasks; using ClientCore.Enums; +using ClientCore.Extensions; using Localization; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; @@ -1512,11 +1513,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 1f122dbe4..3cd4aae55 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("UI:Main:Error"), "Unable to copy link".L10N("UI:Main:ClipboardCopyLinkFailed")); } } diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs index fa8842e0a..52c0722d9 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -14,6 +14,7 @@ using System.Reflection; using System.Threading.Tasks; using ClientCore.Enums; +using ClientCore.Extensions; using Localization; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index 19d2dc51b..c4b07af94 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 f3814f443..ee12020f2 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 Localization; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index fd5a51644..87cdd7a64 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 Localization; @@ -648,7 +649,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("UI:Main:DeleteMapFailedTitle"), "Deleting map failed! Reason:".L10N("UI:Main:DeleteMapFailedText") + " " + ex.Message); } @@ -943,7 +944,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 43418ae59..5b87de15f 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("UI: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 b4ef974d5..f2fc50487 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 Localization; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs index f33efbd30..0c2e9dfae 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -297,7 +297,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 8cb534a7e..979a97ae8 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("UI: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 f2c986935..be2b1e7a3 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("UI: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("UI: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 046b1a158..2046b2a37 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -10,6 +10,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; @@ -405,8 +406,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; } } @@ -432,9 +432,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(); } } @@ -606,9 +606,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; } } @@ -862,15 +862,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 7b5d7dcee..9eb948f76 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -214,8 +214,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 be963a75a..a569dc666 100644 --- a/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs +++ b/DXMainClient/Domain/Multiplayer/MultiplayerColor.cs @@ -62,9 +62,9 @@ public static List LoadColors() 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 4aa515be1..664bc6154 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 aadabf0e9..0aff42f66 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 30e2e418f..b7577a2c7 100644 --- a/DXMainClient/PreStartup.cs +++ b/DXMainClient/PreStartup.cs @@ -50,9 +50,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); @@ -73,9 +73,24 @@ public static void Initialize(StartupParams parameters) clientLogFile.Delete(); - 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 @@ -118,7 +133,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."); TranslationTable.Instance = new TranslationTable(); } @@ -145,7 +160,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 @@ -159,7 +174,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." @@ -176,78 +191,6 @@ 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("UI:Main:FatalErrorText1") + Environment.NewLine + Environment.NewLine + - ex.Message + Environment.NewLine + Environment.NewLine + (crashLogCopied ? - "A crash log has been saved to the following file:".L10N("UI: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("UI:Main:FatalErrorText3") : - "If the issue is repeatable, contact the {1} staff at {2}.".L10N("UI:Main:FatalErrorText4")), - MainClientConstants.GAME_NAME_LONG, - MainClientConstants.GAME_NAME_SHORT, - MainClientConstants.SUPPORT_URL_SHORT); - - ProgramConstants.DisplayErrorAction("KABOOOOOOOM".L10N("UI:Main:FatalErrorTitle"), error, true); - } - [SupportedOSPlatform("windows")] private static void CheckPermissions() { @@ -257,7 +200,7 @@ private static void CheckPermissions() string error = string.Format(("You seem to be running {0} from a write-protected directory." + Environment.NewLine + Environment.NewLine + "For {1} to function properly when run from a write-protected directory, it needs administrative priveleges." + Environment.NewLine + Environment.NewLine + "Would you like to restart the client with administrative rights?" + Environment.NewLine + Environment.NewLine + - "Please also make sure that your security software isn't blocking {1}.").L10N("UI:Main:AdminRequiredText"), MainClientConstants.GAME_NAME_LONG, MainClientConstants.GAME_NAME_SHORT); + "Please also make sure that your security software isn't blocking {1}.").L10N("UI:Main:AdminRequiredText"), ProgramConstants.GAME_NAME_LONG, ProgramConstants.GAME_NAME_SHORT); ProgramConstants.DisplayErrorAction("Administrative privileges required".L10N("UI:Main:AdminRequiredTitle"), error, false); @@ -325,6 +268,7 @@ private static bool UserHasDirectoryAccessRights(string path, FileSystemRights a { return false; } + return isInRoleWithAccess; } } diff --git a/DXMainClient/Startup.cs b/DXMainClient/Startup.cs index fe5eecc9d..1407ac33d 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; @@ -47,22 +48,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"); @@ -73,8 +74,9 @@ public void Execute() { updaterFolder.Delete(true); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } } @@ -89,8 +91,9 @@ public void Execute() { savedGamesFolder.Create(); } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); } } } @@ -104,9 +107,9 @@ public void Execute() { SafePath.DeleteFileIfExists(ProgramConstants.GamePath, FormattableString.Invariant($"{component.LocalPath}_u")); } - catch + catch (Exception ex) { - + ProgramConstants.LogException(ex); } } } @@ -158,11 +161,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 + "."); } } } @@ -172,8 +173,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 @@ -227,9 +227,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 + "."); } } @@ -253,10 +253,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"; } @@ -275,8 +276,10 @@ private static void CheckSystemSpecifications() } } } - catch + catch (Exception ex) { + ProgramConstants.LogException(ex); + cpu = "Video controller info not found"; } @@ -293,8 +296,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"; } @@ -335,8 +340,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); @@ -350,7 +356,10 @@ private static async Task GenerateOnlineIdAsync() else str = o.ToString(); } - catch { } + catch (Exception ex1) + { + ProgramConstants.LogException(ex1); + } Connection.SetId(str); } @@ -365,8 +374,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()); } } @@ -392,9 +402,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 9a3cd0254d23fcda215ddfe5f349b79f33bb42b5 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 27 Nov 2022 16:27:26 +0100 Subject: [PATCH 42/71] 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 | 208 ++++++++---------- .../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, 352 insertions(+), 311 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 c579d5570..b166df1bc 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("UI: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("UI: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("UI: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 151e0631f..7344330f1 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -52,8 +52,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; @@ -607,12 +607,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() { @@ -887,7 +887,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)); } @@ -988,7 +988,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("UI:Main:CreateGameNamed"), e.GameRoomName))); @@ -1010,7 +1010,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("UI:Main:CreateGameNamed"), e.GameRoomName))); @@ -1188,8 +1188,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; @@ -1421,7 +1421,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 3cd4aae55..39d910436 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 Localization; @@ -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 52c0722d9..bfca54015 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 Localization; using SixLabors.ImageSharp; using Color = Microsoft.Xna.Framework.Color; @@ -527,7 +528,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 ee12020f2..dfedc8737 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("UI:Main:TunnelInfo"), false, PrintTunnelServerInformation)); - AddChatBoxCommand(new ChatBoxCommand( + AddChatBoxCommand(new( "CHANGETUNNEL", "Change the used CnCNet tunnel server (game host only)".L10N("UI:Main:ChangeTunnel"), true, _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer")))); - AddChatBoxCommand(new ChatBoxCommand( + 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("UI: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; @@ -203,25 +188,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; @@ -230,13 +215,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); @@ -271,7 +256,6 @@ private void GameStartTimer_TimeElapsed(object sender, EventArgs e) AddNotice($"Some players ({playerString}) failed to connect within the time limit. " + $"Aborting game launch."); - AbortGameStart(); } @@ -317,6 +301,8 @@ public async Task SetUpAsync(Channel channel, bool isHost, int playerLimit, btnChangeTunnel.Disable(); } + tunnels = tunnelHandler.Tunnels; + tunnelHandler.CurrentTunnel = tunnel; tunnelHandler.CurrentTunnelPinged += tunnelHandler_CurrentTunnelFunc; @@ -335,13 +321,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)); @@ -351,7 +337,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); @@ -367,7 +353,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) @@ -408,7 +394,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); @@ -520,7 +506,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("UI:Main:HostAbandoned"))); await BtnLeaveGame_LeftClickAsync(); } @@ -536,7 +522,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("UI:Main:HostAbandoned"))); await BtnLeaveGame_LeftClickAsync(); } @@ -550,7 +536,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("UI:Main:YouWereKicked"))); await ClearAsync(); Visible = false; @@ -575,7 +561,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("UI:Main:HostHasAbandoned"))); await BtnLeaveGame_LeftClickAsync(); } @@ -750,7 +736,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++) { @@ -775,7 +761,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++) @@ -833,12 +819,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); } @@ -920,7 +906,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); } @@ -932,7 +918,7 @@ protected override async Task RequestReadyStatusAsync() "you will be unable to participate in the match.").L10N("UI: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; } @@ -945,10 +931,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. @@ -1041,7 +1027,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) @@ -1217,7 +1203,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)); @@ -1423,7 +1409,7 @@ private async Task RequestMapAsync() AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("UI: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("UI:Main:MapSharingDisabledNotice")); - await channel.SendCTCPMessageAsync(MAP_SHARING_DISABLED_MESSAGE, QueuedMessageType.SYSTEM_MESSAGE, 9); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DISABLED, QueuedMessageType.SYSTEM_MESSAGE, 9); } } @@ -1432,7 +1418,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("UI: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) @@ -1456,7 +1442,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) @@ -1537,7 +1523,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); } @@ -1594,7 +1580,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() @@ -1602,7 +1588,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() @@ -1610,7 +1596,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() @@ -1618,7 +1604,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() @@ -1626,7 +1612,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() @@ -1634,7 +1620,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() @@ -1642,7 +1628,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) @@ -1650,7 +1636,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) @@ -1658,7 +1644,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) @@ -1698,7 +1684,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); } } @@ -1714,7 +1700,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); } @@ -1742,8 +1728,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("UI:Main:UnlockGame"); @@ -1752,8 +1738,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) @@ -1838,7 +1824,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("UI: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; } @@ -1853,7 +1839,7 @@ private async Task MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("UI: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) @@ -1884,7 +1870,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("UI: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); } } @@ -1898,7 +1884,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("UI: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); } } @@ -1909,7 +1895,7 @@ private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) AddNotice(string.Format("Uploading map {0} to the CnCNet map database complete.".L10N("UI: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); } } @@ -2080,7 +2066,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); @@ -2114,7 +2100,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 5b87de15f..f84cce3e0 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 979a97ae8..c12b80576 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 be2b1e7a3..07f068dbf 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 ce963cefd..05b34af50 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("UI:Main:InvalidPlayerExtraOptionsMessage"); private static string MAPPING_ERROR_PREFIX => "Auto Allying:".L10N("UI:Main:AutoAllyingPrefix"); - protected static string NOT_ALL_MAPPINGS_ASSIGNED => MAPPING_ERROR_PREFIX + " " + "You must have all mappings assigned.".L10N("UI:Main:NotAllMappingsAssigned"); protected static string MULTIPLE_MAPPINGS_ASSIGNED_TO_SAME_START => MAPPING_ERROR_PREFIX + " " + "Multiple mappings assigned to the same start location.".L10N("UI:Main:MultipleMappingsAssigned"); protected static string ONLY_ONE_TEAM => MAPPING_ERROR_PREFIX + " " + "You must have more than one team assigned.".L10N("UI: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 cd7928b8a..0b02dbd44 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 Localization; @@ -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 664bc6154..2bc053a43 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 0aff42f66..afd8969d6 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 a1b1706d899bb4c41685b821dfb12e1a7b78f31e Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 29 Nov 2022 13:23:16 +0100 Subject: [PATCH 43/71] Dynamic V3 tunnel selection --- ClientCore/ClientConfiguration.cs | 9 +- ClientCore/Extensions/TaskExtensions.cs | 7 +- ClientCore/ProgramConstants.cs | 4 +- ClientCore/Settings/UserINISettings.cs | 41 + DXMainClient/DXGUI/Generic/MainMenu.cs | 7 +- .../DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs | 56 +- .../DXGUI/Multiplayer/GameInformationPanel.cs | 2 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 2 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 3432 +++++++++-------- .../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, 2491 insertions(+), 2213 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 4be547229..6b7403b39 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -47,14 +47,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 a9ea75dd7..b86d274ee 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -48,6 +48,7 @@ public static class ProgramConstants public const string SAVED_GAME_SPAWN_INI = SAVED_GAMES_DIRECTORY + "/spawnSG.ini"; public const string SAVED_GAMES_DIRECTORY = "Saved Games"; 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; @@ -87,9 +88,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 9e3a64cfc..97a7bd16b 100644 --- a/ClientCore/Settings/UserINISettings.cs +++ b/ClientCore/Settings/UserINISettings.cs @@ -98,6 +98,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); @@ -131,17 +134,29 @@ protected UserINISettings(IniFile iniFile) /*********/ public IntSetting IngameScreenWidth { get; private set; } + public IntSetting IngameScreenHeight { get; private set; } + public StringSetting ClientTheme { get; private set; } + 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; } /*********/ @@ -149,12 +164,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; } /********/ @@ -162,8 +184,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; } /************************/ @@ -173,15 +198,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; } @@ -196,6 +229,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 */ /*********************/ @@ -219,7 +258,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 f75cf9fb0..82b1addbf 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -614,13 +614,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 7344330f1..32bd3c542 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; @@ -879,7 +880,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; @@ -986,7 +987,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)); @@ -1408,7 +1409,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..]; @@ -1436,8 +1437,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]; @@ -1447,29 +1450,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 4c30ab9cb..e891f2b7a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameInformationPanel.cs @@ -100,7 +100,7 @@ public void SetInfo(GenericHostedGame game) lblGameVersion.Visible = true; lblHost.Text = "Host:".L10N("UI:Main:GameInfoHost") + " " + Renderer.GetSafeString(game.HostName, lblHost.FontIndex); lblHost.Visible = true; - lblPing.Text = game.Ping > 0 ? "Ping:".L10N("UI:Main:GameInfoPing") + " " + game.Ping.ToString() + " ms" : "Ping: Unknown".L10N("UI:Main:GameInfoPingUnknown"); + lblPing.Text = game.Ping > 0 ? "Ping:".L10N("UI:Main:GameInfoPing") + " " + game.Ping + " ms" : "Ping: Unknown".L10N("UI:Main:GameInfoPingUnknown"); lblPing.Visible = true; lblPlayers.Visible = true; lblPlayers.Text = "Players".L10N("UI:Main:GameInfoPlayers") + " (" + game.Players.Length + " / " + game.MaxPlayers + "):"; diff --git a/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLoadingLobbyBase.cs index c4b07af94..998721774 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 dfedc8737..dac90127f 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -1,2146 +1,2330 @@ +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; using DTAClient.Online; using DTAClient.Online.EventArguments; +using Localization; using Microsoft.Xna.Framework; 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 Localization; -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; + + 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; + 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("UI:Main:TunnelInfo"), + false, + PrintTunnelServerInformation)); + AddChatBoxCommand(new( + CnCNetLobbyCommands.CHANGETUNNEL, + "Change the used CnCNet tunnel server (game host only)".L10N("UI:Main:ChangeTunnel"), + true, + _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI: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("UI:Main:DownloadMapCommandDescription"), + false, + DownloadMapByIdCommand)); + } - 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; + public event EventHandler GameLeft; - private const double MAX_TIME_FOR_GAME_LAUNCH = 20.0; + public override void Initialize() + { + IniNameOverride = nameof(CnCNetGameLobby); - private static readonly Color ERROR_MESSAGE_COLOR = Color.Yellow; + base.Initialize(); - #region Priorities + btnChangeTunnel = FindChild(nameof(btnChangeTunnel)); - private const int PRIORITY_START_GAME = 10; + btnChangeTunnel.LeftClick += BtnChangeTunnel_LeftClick; - #endregion + gameBroadcastTimer = new(WindowManager) + { + AutoReset = true, + Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL), + Enabled = false + }; + gameBroadcastTimer.TimeElapsed += (_, _) => BroadcastGameAsync().HandleTask(); - 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) + gameStartTimer = new(WindowManager) { - this.connectionManager = connectionManager; - localGame = ClientConfiguration.Instance.LocalGame; - this.tunnelHandler = tunnelHandler; - this.gameCollection = gameCollection; - this.cncnetUserData = cncnetUserData; - this.pmWindow = pmWindow; + AutoReset = false, + Interval = TimeSpan.FromSeconds(MAX_TIME_FOR_GAME_LAUNCH) + }; + gameStartTimer.TimeElapsed += GameStartTimer_TimeElapsed; - 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()) - }; + tunnelSelectionWindow = new(WindowManager, tunnelHandler); - 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()); + tunnelSelectionWindow.Initialize(); - AddChatBoxCommand(new( - "TUNNELINFO", - "View tunnel server information".L10N("UI:Main:TunnelInfo"), - false, - PrintTunnelServerInformation)); - AddChatBoxCommand(new( - "CHANGETUNNEL", - "Change the used CnCNet tunnel server (game host only)".L10N("UI:Main:ChangeTunnel"), - true, - _ => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI: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("UI: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; + tunnelSelectionWindow.DrawOrder = 1; + tunnelSelectionWindow.UpdateOrder = 1; - 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; + DarkeningPanel.AddAndInitializeWithControl(WindowManager, tunnelSelectionWindow); + tunnelSelectionWindow.CenterOnParent(); + tunnelSelectionWindow.Disable(); - private XNATimerControl gameBroadcastTimer; - private XNATimerControl gameStartTimer; + tunnelSelectionWindow.TunnelSelected += (_, e) => TunnelSelectionWindow_TunnelSelectedAsync(e).HandleTask(); - private int playerLimit; + mapSharingConfirmationPanel = new(WindowManager); - private bool closed; + MapPreviewBox.AddChild(mapSharingConfirmationPanel); - private bool isCustomPassword; - private bool isP2P; + mapSharingConfirmationPanel.MapDownloadConfirmed += MapSharingConfirmationPanel_MapDownloadConfirmed; - private readonly List tunnelPlayerIds = new(); - private bool[] isPlayerConnectedToTunnel; - private GameTunnelHandler gameTunnelHandler; - private bool isStartingGame; + WindowManager.AddAndInitializeControl(gameBroadcastTimer); - private string gameFilesHash; + globalContextMenu = new(WindowManager, connectionManager, cncnetUserData, pmWindow); - private readonly List hostUploadedMaps = new(); - private readonly List chatCommandDownloadedMaps = new(); - private List tunnels = new(); + AddChild(globalContextMenu); + AddChild(gameStartTimer); - private MapSharingConfirmationPanel mapSharingConfirmationPanel; + MultiplayerNameRightClicked += MultiplayerName_RightClick; - /// - /// The SHA1 of the latest selected map. - /// Used for map sharing. - /// - private string lastMapSHA1; + 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(); - /// - /// The map name of the latest selected map. - /// Used for map sharing. - /// - private string lastMapName; + PostInitialize(); + } - 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 void GameStartTimer_TimeElapsed(object sender, EventArgs e) + { + string playerString = string.Empty; - public override void Initialize() + for (int i = 0; i < Players.Count; i++) { - IniNameOverride = nameof(CnCNetGameLobby); - base.Initialize(); - - 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(WindowManager); - gameBroadcastTimer.AutoReset = true; - gameBroadcastTimer.Interval = TimeSpan.FromSeconds(GAME_BROADCAST_INTERVAL); - gameBroadcastTimer.Enabled = false; - gameBroadcastTimer.TimeElapsed += (_, _) => BroadcastGameAsync().HandleTask(); - - gameStartTimer = new(WindowManager); - gameStartTimer.AutoReset = false; - gameStartTimer.Interval = TimeSpan.FromSeconds(MAX_TIME_FOR_GAME_LAUNCH); - gameStartTimer.TimeElapsed += GameStartTimer_TimeElapsed; - - 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(); + 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("UI: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("UI: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("UI:Main:TunnelUnavailable")); - } - else - { - AddNotice(string.Format("Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("UI: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("UI:Main:TunnelUnavailable")); + } + else { - this.chatColor = chatColor; - tbChatInput.TextColor = chatColor.XnaColor; + AddNotice(string.Format("Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("UI: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("UI: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 = ddPlayerSides[Players.IndexOf(player)].SelectedItem.Text; + + string currentState = ProgramConstants.IsInGame ? "In Game" : "In Lobby"; // not UI strings + + discordHandler.UpdatePresence( + Map.Name, + GameMode.Name, + "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("UI: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("UI:Main:PlayerRename"), e.OldUserName, e.User.Name)); - } + connectionManager.MainChannel.AddMessage( + new(ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("UI: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 = ddPlayerSides[Players.IndexOf(player)].SelectedItem.Text; - 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.Name, GameMode.Name, "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("UI:Main:HostAbandoned"))); + connectionManager.MainChannel.AddMessage( + new(ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:GameRoomNumberLimitReached")); - await LockGameAsync(); - } + else + AddNotice("The game host has locked the game room.".L10N("UI:Main:RoomLockedByHost")); + Locked = true; } - - private async Task RemovePlayerAsync(string playerName) + else if (e.ModeString == "-i") { - AbortGameStart(); + AddNotice("The game room has been unlocked.".L10N("UI: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("UI: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("UI:Main:GameRoomNumberLimitReached")); - else - AddNotice("The game host has locked the game room.".L10N("UI:Main:RoomLockedByHost")); - Locked = true; - } - else if (e.ModeString == "-i") - { - AddNotice("The game room has been unlocked.".L10N("UI: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("UI: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("UI: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." + Environment.NewLine + + "Try picking a different tunnel server:").L10N("UI: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("UI: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("UI: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." + Environment.NewLine + - "Try picking a different tunnel server:").L10N("UI: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("UI: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("UI: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("UI: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; + sb.Append(Convert.ToInt32(pInfo.Ready)); - int playerOptions = Conversions.IntFromString(parts[i + 1], -1); - if (playerOptions == -1) - return; + sb.Append(';'); + } + } - byte[] byteArray = BitConverter.GetBytes(playerOptions); + return channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_PLAYERS_MESSAGE, 11); + } - int team = byteArray[0]; - int start = byteArray[1]; - int color = byteArray[2]; - int side = byteArray[3]; + protected override async Task PlayerExtraOptions_OptionsChangedAsync() + { + await base.PlayerExtraOptions_OptionsChangedAsync(); + await BroadcastPlayerExtraOptionsAsync(); + } - if (side < 0 || side > SideCount + RandomSelectorCount) - return; + protected override async Task BroadcastPlayerExtraOptionsAsync() + { + if (!IsHost) + return; - if (color < 0 || color > MPColors.Count) - return; + PlayerExtraOptions playerExtraOptions = GetPlayerExtraOptions(); - if (start < 0 || start > MAX_PLAYER_COUNT) - return; + await channel.SendCTCPMessageAsync(playerExtraOptions.ToCncnetMessage(), QueuedMessageType.GAME_PLAYERS_EXTRA_MESSAGE, 11, true); + } - if (team < 0 || team > 4) - return; + private Task BroadcastPlayerTunnelPingsAsync() + => channel.SendCTCPMessageAsync(CnCNetCommands.PLAYER_TUNNEL_PINGS + " " + pinnedTunnelPingsMessage, QueuedMessageType.SYSTEM_MESSAGE, 10); - pInfo.TeamId = byteArray[0]; - pInfo.StartingLocation = byteArray[1]; - pInfo.ColorId = byteArray[2]; - pInfo.SideId = byteArray[3]; + /// + /// Handles player option messages received from the game host. + /// + private void ApplyPlayerOptions(string sender, string message) + { + if (sender != hostName) + return; - if (pInfo.IsAI) - { - pInfo.Ready = true; - AIPlayers.Add(pInfo); - i += AI_PLAYER_OPTIONS_LENGTH; - } - else - { - if (parts.Length <= i + 2) - return; + Players.Clear(); + AIPlayers.Clear(); - int readyStatus = Conversions.IntFromString(parts[i + 2], -1); + string[] parts = message.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - if (readyStatus == -1) - return; + for (int i = 0; i < parts.Length;) + { + var pInfo = new PlayerInfo(); + string pName = parts[i]; + int converted = Conversions.IntFromString(pName, -1); - pInfo.Ready = readyStatus > 0; - pInfo.AutoReady = readyStatus > 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.Name); + 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("UI:Main:HostGameOptionInvalid"), Color.Red); - return; + i += HUMAN_PLAYER_OPTIONS_LENGTH; } + } - string mapOfficial = parts[partIndex]; - bool isMapOfficial = Conversions.BooleanFromString(mapOfficial, true); + CopyPlayerDataToUI(); + } - string mapSHA1 = parts[partIndex + 1]; + /// + /// Broadcasts game options to non-host players + /// when the host has changed an option. + /// + protected override async Task OnGameOptionChangedAsync() + { + await base.OnGameOptionChangedAsync(); - string gameMode = parts[partIndex + 2]; + if (!IsHost) + return; - 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("UI:Main:HostChangeFrameSendRate"), frameSendRate)); - } + bool[] optionValues = new bool[CheckBoxes.Count]; + for (int i = 0; i < CheckBoxes.Count; i++) + optionValues[i] = CheckBoxes[i].Checked; - 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("UI:Main:HostChangeMaxAhead"), maxAhead)); - } + // Let's pack the booleans into bytes + var byteList = Conversions.BoolArrayIntoBytes(optionValues).ToList(); - 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("UI:Main:HostChangeProtocolVersion"), protocolVersion)); - } + while (byteList.Count % 4 != 0) + byteList.Add(0); - string mapName = parts[partIndex + 8]; - GameModeMap currentGameModeMap = GameModeMap; + int integerCount = byteList.Count / 4; + byte[] byteArray = byteList.ToArray(); - lastMapSHA1 = mapSHA1; - lastMapName = mapName; + var sb = new ExtendedStringBuilder(CnCNetCommands.GAME_OPTIONS + " ", true, ';'); - GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapSHA1); - if (GameModeMap == null) - { - await ChangeMapAsync(null); + for (int i = 0; i < integerCount; i++) + sb.Append(BitConverter.ToInt32(byteArray, i * 4)); - if (!isMapOfficial) - await RequestMapAsync(); - else - await ShowOfficialMapMissingMessageAsync(mapSHA1); - } - else if (GameModeMap != currentGameModeMap) - { - await ChangeMapAsync(GameModeMap); - } + // 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 - // 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 + foreach (GameLobbyDropDown dd in DropDowns) + sb.Append(dd.SelectedIndex); - // 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 + 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.Name); - for (int i = 0; i < checkBoxIntegerCount; i++) - { - if (parts.Length <= i) - return; + await channel.SendCTCPMessageAsync(sb.ToString(), QueuedMessageType.GAME_SETTINGS_MESSAGE, 11); + } - int checkBoxStatusInt; - bool success = int.TryParse(parts[i], out checkBoxStatusInt); + /// + /// Handles game option messages received from the game host. + /// + private async Task ApplyGameOptionsAsync(string sender, string message) + { + if (sender != hostName) + 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("UI:Main:HostCheckBoxParseError"), Color.Red); - return; - } + string[] parts = message.Split(';'); + int checkBoxIntegerCount = (CheckBoxes.Count / 32) + 1; + int partIndex = checkBoxIntegerCount + DropDowns.Count; - byte[] byteArray = BitConverter.GetBytes(checkBoxStatusInt); - bool[] boolArray = Conversions.BytesIntoBoolArray(byteArray); + 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("UI:Main:HostGameOptionInvalid"), Color.Red); + return; + } - for (int optionIndex = 0; optionIndex < boolArray.Length; optionIndex++) - { - int gameOptionIndex = i * 32 + optionIndex; + 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); - if (gameOptionIndex >= CheckBoxes.Count) - break; + if (frameSendRate != FrameSendRate) + { + FrameSendRate = frameSendRate; - GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; + AddNotice(string.Format("The game host has changed FrameSendRate (order lag) to {0}".L10N("UI:Main:HostChangeFrameSendRate"), frameSendRate)); + } - if (checkBox.Checked != boolArray[optionIndex]) - { - if (boolArray[optionIndex]) - AddNotice(string.Format("The game host has enabled {0}".L10N("UI:Main:HostEnableOption"), checkBox.Text)); - else - AddNotice(string.Format("The game host has disabled {0}".L10N("UI:Main:HostDisableOption"), checkBox.Text)); - } + int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); - CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; - } - } + if (maxAhead != MaxAhead) + { + MaxAhead = 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("UI:Main:HostGameOptionInvalid"), Color.Red); - return; - } + AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("UI:Main:HostChangeMaxAhead"), maxAhead)); + } - int ddSelectedIndex; - bool success = int.TryParse(parts[i], out ddSelectedIndex); + int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); - 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("UI:Main:HostGameOptionInvalidTheSecondTime"), Color.Red); - return; - } + if (protocolVersion != ProtocolVersion) + { + ProtocolVersion = protocolVersion; - GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; + AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("UI:Main:HostChangeProtocolVersion"), protocolVersion)); + } - if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) - continue; + string mapName = parts[partIndex + 8]; + GameModeMap currentGameModeMap = GameModeMap; - if (dd.SelectedIndex != ddSelectedIndex) - { - string ddName = dd.OptionName; - if (dd.OptionName == null) - ddName = dd.Name; + lastMapHash = mapHash; + lastMapName = mapName; - AddNotice(string.Format("The game host has set {0} to {1}".L10N("UI:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); - } + GameModeMap = GameModeMaps.Find(gmm => gmm.GameMode.Name == gameMode && gmm.Map.SHA1 == mapHash); - DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; - } + 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 + + for (int i = 0; i < checkBoxIntegerCount; i++) + { + if (parts.Length <= i) + return; - if (!parseSuccess) + 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("UI: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("UI: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; - RandomSeed = randomSeed; + GameLobbyCheckBox checkBox = CheckBoxes[gameOptionIndex]; + + if (checkBox.Checked != boolArray[optionIndex]) + { + if (boolArray[optionIndex]) + AddNotice(string.Format("The game host has enabled {0}".L10N("UI:Main:HostEnableOption"), checkBox.Text)); + else + AddNotice(string.Format("The game host has disabled {0}".L10N("UI:Main:HostDisableOption"), checkBox.Text)); + } + + 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("UI: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("UI:Main:HostGameOptionInvalid"), Color.Red); + return; } - else + + bool success = int.TryParse(parts[i], out int ddSelectedIndex); + + if (!success) { - AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("UI: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("UI:Main:MapSharingDisabledNotice")); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DISABLED, QueuedMessageType.SYSTEM_MESSAGE, 9); + AddNotice(("Failed to parse drop down options sent by game host (2)! " + + "The game host's game version might be different from yours.").L10N("UI:Main:HostGameOptionInvalidTheSecondTime"), Color.Red); + return; + } + + GameLobbyDropDown dd = DropDowns[i - checkBoxIntegerCount]; + + if (ddSelectedIndex < -1 || ddSelectedIndex >= dd.Items.Count) + continue; + + 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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:StartingGame")); + cncnetUserData.AddRecentPlayers(recentPlayers, channel.UIName); + await StartGameAsync(); + } - isStartingGame = false; + protected override async Task StartGameAsync() + { + AddNotice("Starting game...".L10N("UI: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("UI:Main:PlayerReturned"), sender)); + private void ReturnNotification(string sender) + { + AddNotice(string.Format("{0} has returned from the game.".L10N("UI: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) + private void HandleTunnelPing(string sender, int ping) + { + PlayerInfo pInfo = Players.Find(p => p.Name.Equals(sender)); + + 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("UI: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("UI: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) + { + AddNotice("You've locked the game room.".L10N("UI:Main:RoomLockedByYou")); + await LockGameAsync(); + } + else { - if (!Locked) + if (Players.Count < playerLimit) { - AddNotice("You've locked the game room.".L10N("UI:Main:RoomLockedByYou")); - await LockGameAsync(); + AddNotice("You've unlocked the game room.".L10N("UI:Main:RoomUnockedByYou")); + await UnlockGameAsync(false); } else { - if (Players.Count < playerLimit) - { - AddNotice("You've unlocked the game room.".L10N("UI:Main:RoomUnockedByYou")); - await UnlockGameAsync(false); - } - else - AddNotice(string.Format( - "Cannot unlock game; the player limit ({0}) has been reached.".L10N("UI:Main:RoomCantUnlockAsLimit"), playerLimit)); + AddNotice(string.Format("Cannot unlock game; the player limit ({0}) has been reached.".L10N("UI: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("UI: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("UI:Main:UnlockGame"); + AccelerateGameBroadcasting(); + } - Locked = false; - if (announce) - AddNotice("The game room has been unlocked.".L10N("UI:Main:GameRoomUnlocked")); - btnLockGame.Text = "Lock Game".L10N("UI: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("UI:Main:GameRoomUnlocked")); - AddNotice(string.Format("Kicking {0} from the game...".L10N("UI:Main:KickPlayer"), pInfo.Name)); - await channel.SendKickMessageAsync(pInfo.Name, 8); - } + btnLockGame.Text = "Lock Game".L10N("UI: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("UI:Main:KickPlayer"), pInfo.Name)); + await channel.SendKickMessageAsync(pInfo.Name, 8); + } - if (user != null) - { - AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("UI: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("UI: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("UI: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("UI:Main:PlayerModifyFileCheat"), sender), Color.Red); - 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! " + - "The game host needs to change the server or you will be unable " + - "to participate in the match.").L10N("UI:Main:HostInvalidTunnel"), - Color.Yellow); - btnLaunchGame.AllowClick = false; - return; - } + private async Task HandleTunnelServerChangeMessageAsync(string sender, string tunnelAddressAndPort) + { + if (sender != hostName) + return; - await HandleTunnelServerChangeAsync(tunnel); - btnLaunchGame.AllowClick = true; - } + 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); - /// - /// Changes the tunnel server used for the game. - /// - /// The new tunnel server to use. - private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) + if (tunnel == null) { - tunnelHandler.CurrentTunnel = tunnel; - AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("UI:Main:HostChangeTunnel"), tunnel.Name)); - return UpdatePingAsync(); + 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("UI:Main:HostInvalidTunnel"), + Color.Yellow); + + btnLaunchGame.AllowClick = false; + return; } - #region CnCNet map sharing + await HandleTunnelServerChangeAsync(tunnel); - 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)) - { - 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("UI:Main:DownloadCustomMapFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); + btnLaunchGame.AllowClick = true; + } - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - return; - } + private void HandleTunnelPingsMessage(string sender, string tunnelPingsMessage) + { + (string Name, CnCNetTunnel Tunnel) playerTunnelInfo = playerTunnels.SingleOrDefault(p => p.Name.Equals(sender, StringComparison.OrdinalIgnoreCase)); - 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("UI:Main:DownloadMapCommandFailedGeneric")); - mapSharingConfirmationPanel.SetFailedStatus(); - return; - } + if (playerTunnelInfo.Tunnel is not null) + return; - AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("UI:Main:RequestHostUploadMapToDB")); + tunnelPingsMessages.Add((sender, tunnelPingsMessage)); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } + if (!pinnedTunnels.Any()) + return; - private async Task MapSharer_HandleMapDownloadCompleteAsync(SHA1EventArgs e) + string[] tunnelPingsLines = tunnelPingsMessage.Split('\t', StringSplitOptions.RemoveEmptyEntries); + IEnumerable<(int Ping, string Hash)> tunnelPings = tunnelPingsLines.Select(q => { - 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("UI: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("UI:Main:MapTransferFailed")); - mapSharingConfirmationPanel.SetFailedStatus(); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } - } + string[] split = q.Split(';'); - private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) - { - Map map = e.Map; + 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(); - hostUploadedMaps.Add(map.SHA1); + if (hash is null) + { + AddNotice(string.Format("No common tunnel server found for: {0}".L10N("UI:Main:NoCommonTunnel"), sender)); + } + else + { + CnCNetTunnel tunnel = tunnelHandler.Tunnels.Single(q => q.Hash.Equals(hash, StringComparison.OrdinalIgnoreCase)); - AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("UI: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("UI:Main:YouMustReplaceMap")); - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } + playerTunnels.Add(new(sender, tunnel)); + AddNotice(string.Format("Dynamic tunnel server negotiated with {0}: {1} ({2}ms)".L10N("UI:Main:TunnelNegotiated"), sender, tunnel.Name, tunnel.PingInMs)); } + } - private async Task MapSharer_HandleMapUploadCompleteAsync(MapEventArgs e) - { - hostUploadedMaps.Add(e.Map.SHA1); + /// + /// 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("Uploading map {0} to the CnCNet map database complete.".L10N("UI:Main:UpdateMapToDBSuccess"), e.Map.Name)); - if (e.Map == Map) - { - await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DOWNLOAD + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); - } + AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("UI:Main:HostChangeTunnel"), tunnel.Name)); + return UpdatePingAsync(); + } + + 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)) + { + 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("UI:Main:DownloadCustomMapFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.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 mapSHA1) + if (chatCommandDownloadedMaps.Contains(e.SHA1)) { - if (hostUploadedMaps.Contains(mapSHA1)) - { - Logger.Log("HandleMapUploadRequest: Map " + mapSHA1 + " is already uploaded!"); - return; - } + // 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("UI:Main:DownloadMapCommandFailedGeneric")); + mapSharingConfirmationPanel.SetFailedStatus(); + return; + } - Map map = null; + AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("UI:Main:RequestHostUploadMapToDB")); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_UPLOAD + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } - foreach (GameMode gm in GameModeMaps.GameModes) - { - map = gm.Maps.Find(m => m.SHA1 == mapSHA1); + 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) - break; - } + if (map != null) + { + AddNotice(returnMessage); - if (map == null) + if (lastMapHash == e.SHA1) { - Logger.Log("Unknown map upload request from " + sender + ": " + mapSHA1); - return; - } + GameModeMap = GameModeMaps.Find(gmm => gmm.Map.SHA1 == lastMapHash); - if (map.Official) - { - Logger.Log("HandleMapUploadRequest: Map is official, so skip request"); + 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("UI: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("UI:Main:MapTransferFailed")); + mapSharingConfirmationPanel.SetFailedStatus(); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + e.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } + } - 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("UI:Main:PlayerMissingMap"), - sender, map.Name)); + private async Task MapSharer_HandleMapUploadFailedAsync(MapEventArgs e) + { + Map map = e.Map; - return; - } + hostUploadedMaps.Add(map.SHA1); + AddNotice(string.Format("Uploading map {0} to the CnCNet map database failed.".L10N("UI:Main:UpdateMapToDBFailed"), map.Name)); - if (!IsHost) - return; + if (map == Map) + { + AddNotice("You need to change the map or some players won't be able to participate in this match.".L10N("UI:Main:YouMustReplaceMap")); + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_FAIL + " " + map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); + } + } - 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("UI:Main:UpdateMapToDBPrompt"), - sender, map.Name)); + 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("UI:Main:UpdateMapToDBSuccess"), e.Map.Name)); - MapSharer.UploadMap(map, localGame); + if (e.Map == Map) + { + await channel.SendCTCPMessageAsync(CnCNetCommands.MAP_SHARING_DOWNLOAD + " " + Map.SHA1, QueuedMessageType.SYSTEM_MESSAGE, 9); } + } - /// - /// Handles a map transfer failure message sent by either the player or the game host. - /// - private void HandleMapTransferFailMessage(string sender, string sha1) + /// + /// 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)) { - if (sender == hostName) - { - AddNotice("The game host failed to upload the map to the CnCNet map database.".L10N("UI:Main:HostUpdateMapToDBFailed")); + Logger.Log("HandleMapUploadRequest: Map " + mapHash + " is already uploaded!"); + return; + } - hostUploadedMaps.Add(sha1); + Map map = null; - 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("UI:Main:HostMustChangeMap")); - } + foreach (GameMode gm in GameModeMaps.GameModes) + { + map = gm.Maps.Find(m => m.SHA1 == mapHash); - return; - } + if (map != null) + break; + } - if (lastMapSHA1 == sha1) - { - if (!IsHost) - { - AddNotice(string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("UI:Main:PlayerDownloadMapFailed") + " " + - "The host needs to change the map or {0} won't be able to participate in this match.".L10N("UI:Main:HostNeedChangeMapForPlayer"), sender)); - } - else - { - AddNotice(string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("UI:Main:PlayerDownloadMapFailed") + " " + - "You need to change the map or {0} won't be able to participate in this match.".L10N("UI:Main:YouNeedChangeMapForPlayer"), sender)); - } - } + if (map == null) + { + Logger.Log("Unknown map upload request from " + sender + ": " + mapHash); + return; } - private void HandleMapDownloadRequest(string sender, string sha1) + if (map.Official) { - if (sender != hostName) - return; + 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("UI:Main:PlayerMissingMap"), + sender, + map.Name)); + + return; + } + + if (!IsHost) + return; + 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("UI:Main:UpdateMapToDBPrompt"), + sender, + map.Name)); + MapSharer.UploadMap(map, localGame); + } + + /// + /// 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) + { + AddNotice("The game host failed to upload the map to the CnCNet map database.".L10N("UI: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("UI: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("UI:Main:PlayerMissingMaDisabledSharing"), 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("UI:Main:PlayerDownloadMapFailed") + " " + + "The host needs to change the map or {0} won't be able to participate in this match.".L10N("UI: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("UI:Main:PlayerDownloadMapFailed") + " " + + "You need to change the map or {0} won't be able to participate in this match.".L10N("UI: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("UI: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("UI:Main:PlayerMissingMaDisabledSharing"), + 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("UI: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("UI: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("UI:Main:DownloadMapCommandStartingDownload"), sha1, mapName); - sb.Remove(sb.Length - 1, 1); - sb.Append(";"); - sb.Append(Map.Name); - sb.Append(";"); - sb.Append(GameMode.UIName); - 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("UI: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.Name) + .Append(';') + .Append(GameMode.UIName) + .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("UI: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 87cdd7a64..8f07825a8 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -26,7 +26,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; @@ -57,8 +57,8 @@ public GameLobbyBase( string iniName, MapLoader mapLoader, bool isMultiplayer, - DiscordHandler discordHandler - ) : base(windowManager) + DiscordHandler discordHandler) + : base(windowManager) { _iniSectionName = iniName; MapLoader = mapLoader; @@ -138,7 +138,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; } @@ -897,7 +897,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; } @@ -1256,15 +1258,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); @@ -1272,17 +1271,22 @@ private PlayerHouseInfo[] WriteSpawnIni() settings.SetStringValue("UIGameMode", GameMode.UIName); settings.SetStringValue("UIMapName", Map.Name); 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); @@ -1293,24 +1297,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++) @@ -1318,7 +1318,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; @@ -1351,7 +1351,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); @@ -1363,6 +1362,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); } @@ -1379,8 +1379,8 @@ private PlayerHouseInfo[] WriteSpawnIni() if (startingWaypoint > -1) { int multiIndex = pId + 1; - spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, - startingWaypoint); + + spawnIni.SetIntValue("SpawnLocations", "Multi" + multiIndex, startingWaypoint); } } @@ -1391,8 +1391,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); } } @@ -1474,7 +1474,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); } @@ -1681,7 +1681,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++) { @@ -1739,7 +1739,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(); } @@ -1803,7 +1803,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; @@ -2120,7 +2120,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 a9ca89dc6..fce7cac39 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 f2fc50487..83197fb41 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 Localization; @@ -40,22 +41,22 @@ public MultiplayerGameLobby( chatBoxCommands = new List { - new("HIDEMAPS", "Hide map list (game host only)".L10N("UI:Main:ChatboxCommandHideMapsHelp"), true, + new(CnCNetLobbyCommands.HIDEMAPS, "Hide map list (game host only)".L10N("UI:Main:ChatboxCommandHideMapsHelp"), true, s => HideMapList()), - new("SHOWMAPS", "Show map list (game host only)".L10N("UI:Main:ChatboxCommandShowMapsHelp"), true, + new(CnCNetLobbyCommands.SHOWMAPS, "Show map list (game host only)".L10N("UI:Main:ChatboxCommandShowMapsHelp"), true, s => ShowMapList()), - new("FRAMESENDRATE", "Change order lag / FrameSendRate (default 7) (game host only)".L10N("UI:Main:ChatboxCommandFrameSendRateHelp"), true, + new(CnCNetLobbyCommands.FRAMESENDRATE, "Change order lag / FrameSendRate (default 7) (game host only)".L10N("UI:Main:ChatboxCommandFrameSendRateHelp"), true, s => SetFrameSendRateAsync(s).HandleTask()), - new("MAXAHEAD", "Change MaxAhead (default 0) (game host only)".L10N("UI:Main:ChatboxCommandMaxAheadHelp"), true, + new(CnCNetLobbyCommands.MAXAHEAD, "Change MaxAhead (default 0) (game host only)".L10N("UI:Main:ChatboxCommandMaxAheadHelp"), true, s => SetMaxAheadAsync(s).HandleTask()), - new("PROTOCOLVERSION", "Change ProtocolVersion (default 2) (game host only)".L10N("UI:Main:ChatboxCommandProtocolVersionHelp"), true, + new(CnCNetLobbyCommands.PROTOCOLVERSION, "Change ProtocolVersion (default 2) (game host only)".L10N("UI:Main:ChatboxCommandProtocolVersionHelp"), true, s => SetProtocolVersionAsync(s).HandleTask()), - new("LOADMAP", "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("UI:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), - new("RANDOMSTARTS", "Enables completely random starting locations (Tiberian Sun based games only).".L10N("UI:Main:ChatboxCommandRandomStartsHelp"), true, + new(CnCNetLobbyCommands.LOADMAP, "Load a custom map with given filename from /Maps/Custom/ folder.".L10N("UI:Main:ChatboxCommandLoadMapHelp"), true, LoadCustomMap), + new(CnCNetLobbyCommands.RANDOMSTARTS, "Enables completely random starting locations (Tiberian Sun based games only).".L10N("UI:Main:ChatboxCommandRandomStartsHelp"), true, s => SetStartingLocationClearanceAsync(s).HandleTask()), - new("ROLL", "Roll dice, for example /roll 3d6".L10N("UI:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType).HandleTask()), - new("SAVEOPTIONS", "Save game option preset so it can be loaded later".L10N("UI:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), - new("LOADOPTIONS", "Load game option preset".L10N("UI:Main:ChatboxCommandLoadOptionsHelp"), true, presetName => HandleGameOptionPresetLoadCommandAsync(presetName).HandleTask()) + new(CnCNetLobbyCommands.ROLL, "Roll dice, for example /roll 3d6".L10N("UI:Main:ChatboxCommandRollHelp"), false, dieType => RollDiceCommandAsync(dieType).HandleTask()), + new(CnCNetLobbyCommands.SAVEOPTIONS, "Save game option preset so it can be loaded later".L10N("UI:Main:ChatboxCommandSaveOptionsHelp"), false, HandleGameOptionPresetSaveCommand), + new(CnCNetLobbyCommands.LOADOPTIONS, "Load game option preset".L10N("UI: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); @@ -582,7 +582,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 cf1f26c1c..ac9ef2515 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 0b02dbd44..01d653fc4 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 afd8969d6..644ee7e4b 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 9a29aef5feb6f31e30e8661164d975e1907cb774 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 29 Nov 2022 17:41:09 +0100 Subject: [PATCH 44/71] 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 dac90127f..86de466e4 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -35,6 +35,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; @@ -90,6 +91,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, @@ -164,6 +166,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("UI: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; @@ -277,6 +284,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; @@ -300,7 +308,7 @@ public async Task SetUpAsync( AIPlayers.Clear(); } - if (!UserINISettings.Instance.UseDynamicTunnels) + if (!dynamicTunnelsEnabled) { tunnelHandler.CurrentTunnel = tunnel; } @@ -329,11 +337,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); @@ -364,7 +372,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) @@ -389,7 +397,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; @@ -421,7 +429,11 @@ protected override void CopyPlayerDataToUI() private void PrintTunnelServerInformation(string s) { - if (tunnelHandler.CurrentTunnel == null) + if (dynamicTunnelsEnabled) + { + AddNotice("Dynamic tunnels enabled".L10N("UI:Main:DynamicTunnelsEnabled")); + } + else if (tunnelHandler.CurrentTunnel is null) { AddNotice("Tunnel server unavailable!".L10N("UI:Main:TunnelUnavailable")); } @@ -637,7 +649,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(); @@ -767,7 +779,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!"); @@ -881,29 +893,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)); } } @@ -914,7 +926,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; @@ -1019,7 +1031,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); @@ -1333,6 +1345,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; @@ -1344,7 +1357,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++) @@ -1353,7 +1365,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); @@ -1366,10 +1377,17 @@ protected override async Task OnGameOptionChangedAsync() sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.Name); + 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. /// @@ -1382,7 +1400,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("UI:Main:HostGameOptionInvalid"), Color.Red); @@ -1538,6 +1556,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() @@ -1969,6 +2013,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 e41bdc8d5e28267fd13c8d1846a2d99c1b7fa7ef Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 29 Nov 2022 18:54:11 +0100 Subject: [PATCH 45/71] 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 b166df1bc..94528a402 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(lblGameModeValue.Text); 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 32bd3c542..32407d34a 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -1463,31 +1463,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 9492cdb1e..c8b80095c 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("UI: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 6dca0c65c..8dcfa8a86 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 86de466e4..d8bc99203 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -68,6 +68,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. @@ -142,7 +143,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) }; @@ -308,9 +309,11 @@ public async Task SetUpAsync( AIPlayers.Clear(); } + initialTunnel = tunnel; + if (!dynamicTunnelsEnabled) { - tunnelHandler.CurrentTunnel = tunnel; + tunnelHandler.CurrentTunnel = initialTunnel; } else { @@ -346,11 +349,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( @@ -450,7 +448,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); @@ -1386,6 +1384,9 @@ private async Task ToggleDynamicTunnelsAsync() { await ChangeDynamicTunnelsSettingAsync(!dynamicTunnelsEnabled); await OnGameOptionChangedAsync(); + + if (!dynamicTunnelsEnabled) + await TunnelSelectionWindow_TunnelSelectedAsync(new TunnelEventArgs(initialTunnel)); } /// @@ -1560,20 +1561,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 @@ -1967,15 +1966,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("UI: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 9e8c40dde51748a985c50799f6b95a3df5fc4cbd Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Fri, 2 Dec 2022 14:52:38 +0100 Subject: [PATCH 46/71] 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 07f068dbf..1305f7c4e 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("UI: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 7483b44596e4c8352b0e2d96d31da5fee46658fe Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 17:11:08 +0100 Subject: [PATCH 47/71] Support P2P and UPnP port forwarding --- BuildScripts/Common.ps1 | 4 +- ClientCore/Extensions/TaskExtensions.cs | 93 ++- .../PreprocessorBackgroundTask.cs | 2 +- ClientGUI/GameProcessLogic.cs | 4 +- DXMainClient/DXGUI/Generic/LoadingScreen.cs | 11 +- .../CnCNet/CnCNetGameLoadingLobby.cs | 3 +- .../DXGUI/Multiplayer/GameLoadingLobbyBase.cs | 4 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 564 +++++++++++++----- .../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 | 64 +- DXMainClient/Startup.cs | 18 +- build/AfterPublish.targets | 6 + 58 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/ClientGUI/GameProcessLogic.cs b/ClientGUI/GameProcessLogic.cs index 2786f0d5d..8f3d6d692 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -134,8 +134,8 @@ public static void StartGameProcess(WindowManager windowManager) if ((RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) && Environment.ProcessorCount > 1 && SingleCoreAffinity) - { - DtaProcess.ProcessorAffinity = (IntPtr)2; + { + DtaProcess.ProcessorAffinity = 2; } } 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 94528a402..ccf631ead 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 998721774..91ebdd69a 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 d8bc99203..73738d13c 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; @@ -36,6 +39,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; @@ -46,12 +50,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; @@ -64,11 +68,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. @@ -93,6 +102,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, @@ -119,9 +130,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()), @@ -138,13 +149,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()); @@ -172,6 +185,11 @@ public CnCNetGameLobby( "Toggle dynamic CnCNet tunnel servers on/off (game host only)".L10N("UI:Main:ChangeDynamicTunnels"), true, _ => ToggleDynamicTunnelsAsync().HandleTask())); + AddChatBoxCommand(new( + CnCNetLobbyCommands.P2P, + "Toggle P2P connections on/off".L10N("UI:Main:ChangeP2P"), + false, + _ => ToggleP2PAsync().HandleTask())); } public event EventHandler GameLeft; @@ -247,7 +265,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; @@ -256,7 +274,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(); } @@ -271,7 +289,8 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick GetCursorPoint()); } - private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer")); + private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) + => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer")); public async Task SetUpAsync( Channel channel, @@ -286,6 +305,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; @@ -371,16 +391,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); @@ -396,7 +410,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 @@ -437,8 +451,9 @@ private void PrintTunnelServerInformation(string s) } else { - AddNotice(string.Format("Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("UI: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("UI:Main:TunnelInfo"), + tunnelHandler.CurrentTunnel.Name, tunnelHandler.CurrentTunnel.Country, tunnelHandler.CurrentTunnel.Clients, tunnelHandler.CurrentTunnel.MaxClients, tunnelHandler.CurrentTunnel.Official)); } } @@ -482,28 +497,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() @@ -528,7 +577,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) { @@ -537,7 +586,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("UI:Main:PlayerRename"), e.OldUserName, e.User.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} changed their name to {1}".L10N("UI:Main:PlayerRename"), e.OldUserName, e.User.Name)); } } @@ -579,7 +628,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("UI:Main:HostAbandoned"))); @@ -604,7 +653,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) { @@ -613,13 +662,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(); } } @@ -627,7 +673,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("UI:Main:HostHasAbandoned"))); @@ -648,7 +694,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 @@ -687,21 +736,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(); @@ -771,14 +817,12 @@ protected override async Task HostLaunchGameAsync() { if (Players.Count > 1) { - AddNotice("Contacting tunnel server...".L10N("UI:Main:ConnectingTunnel")); + AddNotice("Contacting remote hosts...".L10N("UI: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!"); @@ -793,7 +837,7 @@ protected override async Task HostLaunchGameAsync() await StartGameAsync(); } - private async Task HostLaunchGameV2TunnelAsync() + private async Task HostLaunchGameV2Async() { List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); @@ -802,14 +846,23 @@ private async Task HostLaunchGameV2TunnelAsync() ShowTunnelSelectionWindow(("An error occured while contacting " + "the CnCNet tunnel server." + Environment.NewLine + "Try picking a different tunnel server:").L10N("UI: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("UI:Main:ConnectTunnelError2"), + "(accessible by typing /{0} in the chat box).".L10N("UI: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++) { @@ -818,24 +871,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++) { @@ -843,19 +905,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(';'); @@ -868,52 +926,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)); } } @@ -926,17 +1014,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); @@ -945,9 +1038,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("UI:Main:PlayerConnectFailed"), playerName, CnCNetLobbyCommands.CHANGETUNNEL)); AbortGameStart(); } @@ -956,7 +1049,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) { @@ -965,25 +1058,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("UI: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) @@ -994,11 +1087,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; @@ -1014,23 +1109,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); } @@ -1227,12 +1316,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("UI: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(); @@ -1389,12 +1502,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("UI: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("UI: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(';'); @@ -1418,7 +1553,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("UI:Main:HostChangeFrameSendRate"), frameSendRate)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed FrameSendRate (order lag) to {0}".L10N("UI:Main:HostChangeFrameSendRate"), frameSendRate)); } int maxAhead = Conversions.IntFromString(parts[partIndex + 4], MaxAhead); @@ -1427,7 +1562,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) { MaxAhead = maxAhead; - AddNotice(string.Format("The game host has changed MaxAhead to {0}".L10N("UI:Main:HostChangeMaxAhead"), maxAhead)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed MaxAhead to {0}".L10N("UI:Main:HostChangeMaxAhead"), maxAhead)); } int protocolVersion = Conversions.IntFromString(parts[partIndex + 5], ProtocolVersion); @@ -1436,7 +1571,7 @@ private async Task ApplyGameOptionsAsync(string sender, string message) { ProtocolVersion = protocolVersion; - AddNotice(string.Format("The game host has changed ProtocolVersion to {0}".L10N("UI:Main:HostChangeProtocolVersion"), protocolVersion)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed ProtocolVersion to {0}".L10N("UI:Main:HostChangeProtocolVersion"), protocolVersion)); } string mapName = parts[partIndex + 8]; @@ -1498,9 +1633,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("UI:Main:HostEnableOption"), checkBox.Text)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has enabled {0}".L10N("UI:Main:HostEnableOption"), checkBox.Text)); else - AddNotice(string.Format("The game host has disabled {0}".L10N("UI:Main:HostDisableOption"), checkBox.Text)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has disabled {0}".L10N("UI:Main:HostDisableOption"), checkBox.Text)); } CheckBoxes[gameOptionIndex].Checked = boolArray[optionIndex]; @@ -1537,7 +1672,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("UI:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has set {0} to {1}".L10N("UI:Main:HostSetOption"), ddName, dd.Items[ddSelectedIndex].Text)); } DropDowns[i - checkBoxIntegerCount].SelectedIndex = ddSelectedIndex; @@ -1569,9 +1704,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("UI: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("UI:Main:HostDisableDynamicTunnels"))); if (newDynamicTunnelsEnabledValue) { @@ -1629,6 +1764,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) @@ -1648,12 +1786,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(';'); @@ -1720,7 +1858,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); @@ -1741,7 +1879,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(); @@ -1749,7 +1887,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); @@ -1833,7 +1971,7 @@ protected override async Task StillInGameNotificationAsync(int playerIndex) private void ReturnNotification(string sender) { - AddNotice(string.Format("{0} has returned from the game.".L10N("UI:Main:PlayerReturned"), sender)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} has returned from the game.".L10N("UI:Main:PlayerReturned"), sender)); PlayerInfo pInfo = Players.Find(p => p.Name == sender); @@ -1846,7 +1984,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) { @@ -1877,10 +2015,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("UI: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("UI:Main:DifferentFileCheating"), cheaterName), Color.Red); } protected override async Task BroadcastDiceRollAsync(int dieSides, int[] results) @@ -1907,7 +2045,7 @@ protected override async Task HandleLockGameButtonClickAsync() } else { - AddNotice(string.Format("Cannot unlock game; the player limit ({0}) has been reached.".L10N("UI:Main:RoomCantUnlockAsLimit"), playerLimit)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Cannot unlock game; the player limit ({0}) has been reached.".L10N("UI:Main:RoomCantUnlockAsLimit"), playerLimit)); } } } @@ -1943,7 +2081,7 @@ protected override async Task KickPlayerAsync(int playerIndex) PlayerInfo pInfo = Players[playerIndex]; - AddNotice(string.Format("Kicking {0} from the game...".L10N("UI:Main:KickPlayer"), pInfo.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Kicking {0} from the game...".L10N("UI:Main:KickPlayer"), pInfo.Name)); await channel.SendKickMessageAsync(pInfo.Name, 8); } @@ -1957,18 +2095,18 @@ protected override async Task BanPlayerAsync(int playerIndex) if (user != null) { - AddNotice(string.Format("Banning and kicking {0} from the game...".L10N("UI:Main:BanAndKickPlayer"), pInfo.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Banning and kicking {0} from the game...".L10N("UI: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("UI: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("UI: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)); @@ -1989,15 +2127,8 @@ private async Task HandleTunnelServerChangeMessageAsync(string sender, string ha btnLaunchGame.AllowClick = true; } - 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; @@ -2011,24 +2142,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("UI:Main:NoCommonTunnel"), sender)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "No common tunnel server found for: {0}".L10N("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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. /// @@ -2037,7 +2271,7 @@ private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) { tunnelHandler.CurrentTunnel = tunnel; - AddNotice(string.Format("The game host has changed the tunnel server to: {0}".L10N("UI:Main:HostChangeTunnel"), tunnel.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "The game host has changed the tunnel server to: {0}".L10N("UI:Main:HostChangeTunnel"), tunnel.Name)); return UpdatePingAsync(); } @@ -2106,7 +2340,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("UI:Main:UpdateMapToDBFailed"), map.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Uploading map {0} to the CnCNet map database failed.".L10N("UI:Main:UpdateMapToDBFailed"), map.Name)); if (map == Map) { @@ -2118,7 +2352,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("UI:Main:UpdateMapToDBSuccess"), e.Map.Name)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "Uploading map {0} to the CnCNet map database complete.".L10N("UI:Main:UpdateMapToDBSuccess"), e.Map.Name)); if (e.Map == Map) { @@ -2183,7 +2417,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("UI:Main:HostUpdateMapToDBFailed")); hostUploadedMaps.Add(sha1); @@ -2215,7 +2449,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 8f07825a8..679bea8fc 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1327,7 +1327,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++; @@ -1440,7 +1440,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 f84cce3e0..1a7ad37bc 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("UI: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 c12b80576..65f6c86ae 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("UI: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 24b558ac9..aa2107121 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 9eb948f76..69501ae3a 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -163,7 +163,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 644ee7e4b..10cf4c836 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) + if (IsConnected) throw new InvalidOperationException("The client is already connected!".L10N("UI: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("UI: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 1407ac33d..28e16dbb3 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -311,11 +311,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; @@ -334,7 +331,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); @@ -343,14 +340,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 @@ -363,8 +360,6 @@ private static async Task GenerateOnlineIdAsync() Connection.SetId(str); } -#pragma warning restore format -#if !WINFORMS } else { @@ -377,10 +372,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 8b2f35d75..f08ef37a2 100644 --- a/build/AfterPublish.targets +++ b/build/AfterPublish.targets @@ -34,6 +34,7 @@ + @@ -56,6 +57,7 @@ + @@ -65,6 +67,10 @@ + + + + \ No newline at end of file From 8cf8641ad7ad8fc7c2e65f7cab1b34299fa2f711 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 17:38:18 +0100 Subject: [PATCH 48/71] 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 10cf4c836..31a655e8e 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 ef73586d86cc77158b4ad0acd5288cde10b58c39 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 22:06:30 +0100 Subject: [PATCH 49/71] 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 73738d13c..72deb2a8e 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -1320,9 +1320,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 1049bfd9d219b25f07127124164ccfedaed331eb Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sat, 3 Dec 2022 23:03:04 +0100 Subject: [PATCH 50/71] 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 2046b2a37..226f3fadd 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -352,7 +352,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(','); @@ -448,7 +448,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)); @@ -460,7 +460,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); @@ -576,7 +576,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(','); @@ -794,7 +794,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"; @@ -815,8 +815,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 6d3f11806bb88d8fd26837b00c5e2666f0c3b212 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 6 Dec 2022 22:05:28 +0100 Subject: [PATCH 51/71] 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 82b1addbf..6e8f82362 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -523,9 +523,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 7c5384174..da3d085b8 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 ccf631ead..e362eefb7 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("UI: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 32407d34a..372fdabac 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -581,7 +581,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); @@ -606,16 +606,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; @@ -787,7 +787,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) @@ -796,7 +796,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)) @@ -814,7 +814,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)) @@ -862,7 +862,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("UI:Main:AttemptJoin"), hg.RoomName))); @@ -892,13 +892,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("UI:Main:GameLocked"))); var channel = (Channel)sender; @@ -922,13 +922,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("UI: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; @@ -941,7 +941,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(); @@ -971,7 +971,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; @@ -1000,7 +1000,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; @@ -1022,7 +1022,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; @@ -1031,7 +1031,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; @@ -1063,7 +1063,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; @@ -1115,7 +1115,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; @@ -1162,7 +1162,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(';'); @@ -1231,7 +1231,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) @@ -1265,7 +1265,7 @@ private void HandleGameInvitationFailedNotification(string sender) } } - private async Task DdCurrentChannel_SelectedIndexChangedAsync() + private async ValueTask DdCurrentChannel_SelectedIndexChangedAsync() { if (currentChatChannel != null) { @@ -1531,7 +1531,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) { @@ -1648,7 +1648,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 39d910436..985ae3ba6 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 bfca54015..eb678df7c 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -518,7 +518,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 91ebdd69a..a8e82dee1 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("UI: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("UI: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) @@ -477,7 +477,7 @@ protected void CopyPlayerDataToUI() } } - private async Task DdSavedGame_SelectedIndexChangedAsync() + private async ValueTask DdSavedGame_SelectedIndexChangedAsync() { if (!IsHost) return; @@ -492,7 +492,7 @@ private async Task DdSavedGame_SelectedIndexChangedAsync() UpdateDiscordPresence(); } - private async Task TbChatInput_EnterPressedAsync() + private async ValueTask TbChatInput_EnterPressedAsync() { if (string.IsNullOrEmpty(tbChatInput.Text)) return; @@ -505,9 +505,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 72deb2a8e..d1fd6fd58 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -40,6 +40,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; @@ -55,7 +56,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; @@ -292,7 +293,7 @@ private void MultiplayerName_RightClick(object sender, MultiplayerNameRightClick private void BtnChangeTunnel_LeftClick(object sender, EventArgs e) => ShowTunnelSelectionWindow("Select tunnel server:".L10N("UI:Main:SelectTunnelServer")); - public async Task SetUpAsync( + public async ValueTask SetUpAsync( Channel channel, bool isHost, int playerLimit, @@ -349,7 +350,7 @@ public async Task SetUpAsync( Refresh(isHost); } - public async Task OnJoinedAsync() + public async ValueTask OnJoinedAsync() { var fhc = new FileHashCalculator(); @@ -372,7 +373,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)); @@ -405,7 +406,7 @@ await connectionManager.SendCustomMessageAsync(new( UpdateDiscordPresence(true); } - private async Task UpdatePingAsync() + private async ValueTask UpdatePingAsync() { int ping; @@ -460,7 +461,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}", @@ -475,7 +476,7 @@ public void ChangeChatColor(IRCColor chatColor) tbChatInput.TextColor = chatColor.XnaColor; } - public override async Task ClearAsync() + public override async ValueTask ClearAsync() { await base.ClearAsync(); @@ -520,14 +521,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) { @@ -541,9 +540,7 @@ private async Task CloseP2PPortsAsync() try { foreach (ushort p2pIpV6PortId in p2pIpV6PortIds) - { await internetGatewayDevice.CloseIpV6PortAsync(p2pIpV6PortId); - } } catch (Exception ex) { @@ -555,7 +552,7 @@ private async Task CloseP2PPortsAsync() } } - public async Task LeaveGameLobbyAsync() + public async ValueTask LeaveGameLobbyAsync() { if (IsHost) { @@ -567,7 +564,7 @@ public async Task LeaveGameLobbyAsync() await channel.LeaveAsync(); } - private async Task HandleConnectionLossAsync() + private async ValueTask HandleConnectionLossAsync() { await ClearAsync(); Disable(); @@ -590,7 +587,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) @@ -624,7 +621,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); @@ -640,7 +637,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) { @@ -669,7 +666,7 @@ private async Task Channel_UserKickedAsync(UserNameEventArgs e) } } - private async Task Channel_UserListReceivedAsync() + private async ValueTask Channel_UserListReceivedAsync() { if (!IsHost) { @@ -684,7 +681,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); @@ -732,7 +729,7 @@ private async Task Channel_UserAddedAsync(ChannelUserEventArgs e) } } - private async Task RemovePlayerAsync(string playerName) + private async ValueTask RemovePlayerAsync(string playerName) { AbortGameStart(); @@ -813,7 +810,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) { @@ -837,7 +834,7 @@ protected override async Task HostLaunchGameAsync() await StartGameAsync(); } - private async Task HostLaunchGameV2Async() + private async ValueTask HostLaunchGameV2Async() { List playerPorts = await tunnelHandler.CurrentTunnel.GetPlayerPortInfoAsync(Players.Count); @@ -878,7 +875,7 @@ private string SetGamePlayerPortsV2(IReadOnlyList playerPorts) return sb.ToString(); } - private async Task HostLaunchGameV3Async() + private async ValueTask HostLaunchGameV3Async() { btnLaunchGame.InputEnabled = false; @@ -950,16 +947,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)); } @@ -972,21 +969,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); } } @@ -996,10 +996,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)); } @@ -1010,11 +1010,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 @@ -1030,7 +1030,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); @@ -1044,7 +1044,7 @@ private void HandleTunnelFail(string playerName) AbortGameStart(); } - private async Task HandlePlayerConnectedToTunnelAsync(string playerName) + private async ValueTask HandlePlayerConnectedToTunnelAsync(string playerName) { if (!isStartingGame) return; @@ -1064,37 +1064,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("UI: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; @@ -1124,7 +1118,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 = { @@ -1141,7 +1135,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) { @@ -1170,7 +1164,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; @@ -1234,7 +1228,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; @@ -1254,7 +1248,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 + " "); @@ -1297,13 +1291,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; @@ -1313,19 +1307,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 { @@ -1334,7 +1323,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("UI: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("UI:Main:UPnPP2PFailed")), Color.Orange); return; } @@ -1344,7 +1333,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); /// @@ -1455,7 +1444,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(); @@ -1495,21 +1484,21 @@ protected override async Task OnGameOptionChangedAsync() sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.Name); - 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; @@ -1534,7 +1523,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; @@ -1706,7 +1695,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; @@ -1725,7 +1714,7 @@ private async Task ChangeDynamicTunnelsSettingAsync(bool newDynamicTunnelsEnable } } - private async Task RequestMapAsync() + private async ValueTask RequestMapAsync() { if (UserINISettings.Instance.EnableMapSharing) { @@ -1741,7 +1730,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. " + @@ -1757,7 +1746,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); @@ -1767,7 +1756,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); @@ -1793,7 +1782,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; @@ -1841,7 +1830,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("UI:Main:StartingGame")); @@ -1865,7 +1854,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); @@ -1882,7 +1871,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) { @@ -1900,7 +1889,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 @@ -1912,7 +1901,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(); @@ -1920,7 +1909,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(); @@ -1928,7 +1917,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(); @@ -1936,7 +1925,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(); @@ -1944,7 +1933,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(); @@ -1952,7 +1941,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(); @@ -1960,7 +1949,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); @@ -1968,7 +1957,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); @@ -2001,7 +1990,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; @@ -2028,7 +2017,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("UI: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); @@ -2036,7 +2025,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) { @@ -2057,20 +2046,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("UI: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; @@ -2081,7 +2070,7 @@ await connectionManager.SendCustomMessageAsync( AccelerateGameBroadcasting(); } - protected override async Task KickPlayerAsync(int playerIndex) + protected override async ValueTask KickPlayerAsync(int playerIndex) { if (playerIndex >= Players.Count) return; @@ -2092,7 +2081,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; @@ -2111,7 +2100,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("UI: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; @@ -2156,7 +2145,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("UI:Main:NoCommonTunnel"), playerName)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "No common tunnel found for: {0}".L10N("UI:Main:NoCommonTunnel"), playerName)); } else { @@ -2170,13 +2159,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("UI:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); + AddNotice(string.Format(CultureInfo.CurrentCulture, "{0} dynamic tunnel: {1} ({2}ms)".L10N("UI: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(); @@ -2199,36 +2188,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("UI: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("UI: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("UI:Main:P2PDisabled"), playerName)); + } } private void HandleP2PPingsMessage(string playerName, string p2pPingsMessage) @@ -2242,15 +2233,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(); @@ -2262,19 +2244,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("UI: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("UI: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; @@ -2282,7 +2283,7 @@ private Task HandleTunnelServerChangeAsync(CnCNetTunnel tunnel) return UpdatePingAsync(); } - 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)) @@ -2306,7 +2307,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."); @@ -2342,7 +2343,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; @@ -2356,7 +2357,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("UI:Main:UpdateMapToDBSuccess"), e.Map.Name)); @@ -2559,7 +2560,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 679bea8fc..8d98cad07 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -387,7 +387,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("UI:Main:PresetLoaded")); @@ -401,7 +401,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; @@ -411,7 +411,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; @@ -421,16 +421,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; @@ -626,7 +626,7 @@ protected void RefreshForFavoriteMapRemoved() lbGameModeMapList.SelectedIndex = 0; // the map was removed while viewing favorites } - private async Task DeleteSelectedMapAsync() + private async ValueTask DeleteSelectedMapAsync() { try { @@ -655,7 +655,7 @@ private async Task DeleteSelectedMapAsync() } } - private async Task LbGameModeMapList_SelectedIndexChangedAsync() + private async ValueTask LbGameModeMapList_SelectedIndexChangedAsync() { if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) { @@ -670,7 +670,7 @@ private async Task LbGameModeMapList_SelectedIndexChangedAsync() await ChangeMapAsync(GameModeMap); } - private async Task PickRandomMapAsync() + private async ValueTask PickRandomMapAsync() { int totalPlayerCount = Players.Count(p => p.SideId < ddPlayerSides[0].Items.Count - 1) + AIPlayers.Count; @@ -702,7 +702,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; @@ -871,7 +871,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(); @@ -890,7 +890,7 @@ protected virtual Task PlayerExtraOptions_OptionsChangedAsync() UpdateMapPreviewBoxEnabledStatus(); RefreshBtnPlayerExtraOptionsOpenTexture(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } private void EnablePlayerOptionDropDown(XNAClientDropDown clientDropDown, int playerIndex, bool enable) @@ -959,9 +959,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. @@ -1640,7 +1640,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); @@ -1651,12 +1651,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; @@ -1668,14 +1668,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; @@ -1901,27 +1901,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; @@ -2295,7 +2295,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 1a7ad37bc..c0f54f826 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("UI: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("UI: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 83197fb41..0985a1dc2 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -229,7 +229,7 @@ private void FSWEvent(FileSystemEventArgs e) } } - protected override Task StartGameAsync() + protected override ValueTask StartGameAsync() { if (fsw != null) fsw.EnableRaisingEvents = true; @@ -240,7 +240,7 @@ protected override Task StartGameAsync() return base.StartGameAsync(); } - protected override async Task GameProcessExitedAsync() + protected override async ValueTask GameProcessExitedAsync() { gameSaved = false; @@ -284,7 +284,7 @@ private void GenerateGameID() } } - protected virtual async Task HandleLockGameButtonClickAsync() + protected virtual async ValueTask HandleLockGameButtonClickAsync() { if (Locked) await UnlockGameAsync(true); @@ -292,11 +292,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; @@ -352,7 +352,7 @@ private async Task TbChatInput_EnterPressedAsync() tbChatInput.Text = string.Empty; } - private async Task ChkAutoReady_CheckedChangedAsync() + private async ValueTask ChkAutoReady_CheckedChangedAsync() { btnLaunchGame.Enabled = !chkAutoReady.Checked; await RequestReadyStatusAsync(); @@ -366,7 +366,7 @@ protected void ResetAutoReadyCheckbox() btnLaunchGame.Enabled = true; } - private async Task SetFrameSendRateAsync(string value) + private async ValueTask SetFrameSendRateAsync(string value) { bool success = int.TryParse(value, out int intValue); @@ -384,7 +384,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); @@ -401,7 +401,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); @@ -424,7 +424,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); @@ -455,7 +455,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; @@ -518,7 +518,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. @@ -568,7 +568,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. @@ -706,7 +706,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(); @@ -719,7 +719,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) { @@ -856,43 +856,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("UI: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("UI: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("UI: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("UI: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("UI: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) { @@ -900,19 +900,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("UI: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("UI:Main:InsufficientPlayersNotification1"), @@ -921,29 +921,29 @@ protected virtual Task InsufficientPlayersNotificationAsync() AddNotice(String.Format("Unable to launch game: this map cannot be played with fewer than {0} players.".L10N("UI: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("UI: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(); @@ -951,9 +951,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; @@ -1082,13 +1082,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) @@ -1098,7 +1098,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 0c2e9dfae..e590a8694 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -165,7 +165,7 @@ private string CheckGameValidity() return null; } - protected override async Task BtnLaunchGame_LeftClickAsync() + protected override async ValueTask BtnLaunchGame_LeftClickAsync() { string error = CheckGameValidity(); @@ -179,7 +179,7 @@ protected override async Task BtnLaunchGame_LeftClickAsync() XNAMessageBox.Show(WindowManager, "Cannot launch game".L10N("UI:Main:LaunchGameErrorTitle"), error); } - protected override Task BtnLeaveGame_LeftClickAsync() + protected override ValueTask BtnLeaveGame_LeftClickAsync() { Enabled = false; Visible = false; @@ -189,7 +189,7 @@ protected override Task BtnLeaveGame_LeftClickAsync() topBar.RemovePrimarySwitchable(this); ResetDiscordPresence(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } private void PlayerSideChanged(object sender, EventArgs e) @@ -227,7 +227,7 @@ protected override int GetDefaultMapRankIndex(GameModeMap gameModeMap) return StatisticsManager.Instance.GetSkirmishRankForDefaultMap(gameModeMap.Map.Name, 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 65f6c86ae..a5ca4e881 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 1305f7c4e..0fc4e30db 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 69501ae3a..c74140875 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -130,7 +130,7 @@ private void LoadGameModeAliases(IniFile mpMapsIni) } } - private async Task LoadCustomMapsAsync() + private async ValueTask LoadCustomMapsAsync() { DirectoryInfo customMapsDirectory = SafePath.GetDirectory(ProgramConstants.GamePath, CUSTOM_MAPS_DIRECTORY); @@ -143,7 +143,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) @@ -200,7 +199,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 01d653fc4..f1ce50d55 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 2bc053a43..fa5378636 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 31a655e8e..206a86ae5 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 28e16dbb3..f7baffb73 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -309,7 +309,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 0066afb2e2cd9e65344e99fcc42b7d9ed7ec2d32 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 00:25:28 +0100 Subject: [PATCH 52/71] 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 d1fd6fd58..1d42ad8fe 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -79,6 +79,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. @@ -188,9 +189,11 @@ public CnCNetGameLobby( _ => ToggleDynamicTunnelsAsync().HandleTask())); AddChatBoxCommand(new( CnCNetLobbyCommands.P2P, - "Toggle P2P connections on/off".L10N("UI:Main:ChangeP2P"), + "Toggle P2P connections on/off, your IP will be public to players in the lobby".L10N("UI:Main:ChangeP2P"), false, _ => ToggleP2PAsync().HandleTask())); + + WindowManager.GameClosing += (_, _) => Dispose(true); } public event EventHandler GameLeft; @@ -260,6 +263,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; @@ -513,7 +529,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 d762991b49e7231e4cef3c6b90a8ec05cb879445 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 10:45:11 +0100 Subject: [PATCH 53/71] 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 8f3d6d692..191af0957 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -4,7 +4,7 @@ using ClientCore; using Rampastring.Tools; using ClientCore.INIProcessing; -using System.Threading; +using System.Threading.Tasks; using Rampastring.XNAUI; namespace ClientGUI @@ -26,7 +26,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."); @@ -35,7 +35,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 733151509..faa6b95c2 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("UI: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; @@ -318,7 +311,7 @@ private void LaunchMission(Mission mission) discordHandler.UpdatePresence(mission.GUIName, 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 917a60bb1..95f987ef3 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("UI: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) @@ -219,4 +221,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 6e8f82362..501763f3b 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -225,7 +225,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); @@ -312,10 +312,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); } @@ -379,9 +377,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,21 +977,21 @@ 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(); #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 e362eefb7..f7d34a23c 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 372fdabac..4ca386de3 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -1111,8 +1111,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() @@ -1147,8 +1147,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 a8e82dee1..9cb23cd03 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 8d98cad07..642f4f36b 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1640,7 +1640,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); @@ -1648,10 +1648,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 a5ca4e881..465c64bec 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 594b2ba7826b3ad9c1ebb501c29ae64e3a95dec2 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 11:31:03 +0100 Subject: [PATCH 54/71] 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 9cb23cd03..6c507ed80 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 0985a1dc2..4348dd4f0 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -205,11 +205,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); @@ -225,7 +223,7 @@ private void FSWEvent(FileSystemEventArgs e) gameSaved = true; - SavedGameManager.RenameSavedGame(); + await SavedGameManager.RenameSavedGameAsync(); } } From 600a03bc0ccedbd8cea355066adc6966856d4cc7 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 7 Dec 2022 22:32:33 +0100 Subject: [PATCH 55/71] 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 1d42ad8fe..7a5b86170 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; @@ -984,8 +983,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) { @@ -1000,7 +1004,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); @@ -1330,11 +1334,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) { @@ -1345,7 +1349,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 c0f54f826..92bd585d4 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 465c64bec..99f13b1ba 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 0fc4e30db..85a22fc38 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("UI: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 4e8f3d2cdd568a097404122fd8965d89ff11da04 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 00:09:10 +0100 Subject: [PATCH 56/71] 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 7a5b86170..4a0ddeb38 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -1780,7 +1780,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 bbd4c23403eaf083db188a6a8063aa8e5e02311e Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 00:51:10 +0100 Subject: [PATCH 57/71] 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 c828b10f746ed537ed7d68fb9cdf5179366bc485 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 02:15:07 +0100 Subject: [PATCH 58/71] 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 4a0ddeb38..0b97b80a9 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; @@ -673,12 +672,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() @@ -754,11 +762,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) @@ -2190,22 +2194,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 1bdcab1cb3cc94289a47f9a30127adcb54597655 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 17:20:51 +0100 Subject: [PATCH 59/71] 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 0b97b80a9..9cdd17dbd 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -424,8 +424,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 @@ -539,8 +539,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) { @@ -553,8 +556,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) { @@ -1342,7 +1348,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 1ca032fc7388c1beb63040497ad21a44ff106d17 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 11 Dec 2022 21:23:31 +0100 Subject: [PATCH 60/71] 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 9cdd17dbd..cf0927305 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; @@ -37,7 +38,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; @@ -74,9 +74,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; /// @@ -523,6 +525,7 @@ public override async ValueTask ClearAsync() pinnedTunnelPingsMessage = null; gameStartCancellationTokenSource?.Cancel(); + stunCancellationTokenSource?.Cancel(); v3GameTunnelHandlers.ForEach(q => q.Tunnel.Dispose()); v3GameTunnelHandlers.Clear(); playerTunnels.Clear(); @@ -541,7 +544,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); } } @@ -551,7 +554,7 @@ private async ValueTask CloseP2PPortsAsync() } finally { - p2pPorts.Clear(); + ipV4P2PPorts.Clear(); } try @@ -568,6 +571,7 @@ private async ValueTask CloseP2PPortsAsync() } finally { + ipV6P2PPorts.Clear(); p2pIpV6PortIds.Clear(); } } @@ -991,7 +995,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 @@ -1003,11 +1007,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(); @@ -1099,7 +1117,7 @@ private async ValueTask LaunchGameV3Async() Logger.Log("All players are connected, starting game!"); AddNotice("All players have connected...".L10N("UI: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) { @@ -1123,6 +1141,7 @@ private async ValueTask LaunchGameV3Async() FindLocalPlayer().Port = gamePort; gameStartTimer.Pause(); + stunCancellationTokenSource?.Cancel(); btnLaunchGame.InputEnabled = true; @@ -1342,18 +1361,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("UI: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("UI:Main:UPnPP2PFailed")), + Color.Orange); return; } @@ -1364,7 +1392,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. @@ -2200,8 +2235,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); @@ -2209,7 +2246,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); @@ -2218,13 +2255,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))) @@ -2235,10 +2279,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) { @@ -2283,7 +2327,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 c969a23367c86c8d90ac0f32059395920df27202 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Mon, 12 Dec 2022 18:41:15 +0100 Subject: [PATCH 61/71] 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 cf0927305..cb6d824ca 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -663,7 +663,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("UI:Main:YouWereKicked"))); @@ -674,29 +674,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() @@ -765,19 +753,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(); @@ -2216,13 +2198,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("UI:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } @@ -2275,7 +2251,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 { @@ -2323,7 +2299,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 2986b51f3194d443d7193b96c1fe2a223e0bf606 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Dec 2022 14:15:44 +0100 Subject: [PATCH 62/71] Refactor --- ClientCore/ClientCore.csproj | 4 +- .../Multiplayer/GameLobby/CnCNetGameLobby.cs | 448 ++++-------------- .../Multiplayer/CnCNet/V3ConnectionState.cs | 362 ++++++++++++++ 3 files changed, 465 insertions(+), 349 deletions(-) create mode 100644 DXMainClient/Domain/Multiplayer/CnCNet/V3ConnectionState.cs diff --git a/ClientCore/ClientCore.csproj b/ClientCore/ClientCore.csproj index d51ff4dac..31ed946bf 100644 --- a/ClientCore/ClientCore.csproj +++ b/ClientCore/ClientCore.csproj @@ -46,8 +46,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs index cb6d824ca..5c24c05bd 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; @@ -37,8 +36,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; @@ -52,9 +49,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; @@ -71,15 +65,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. @@ -93,20 +89,6 @@ internal sealed class CnCNetGameLobby : MultiplayerGameLobby /// private string lastMapName; - 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, @@ -260,6 +242,8 @@ public override void Initialize() connectionManager_DisconnectedFunc = (_, _) => HandleConnectionLossAsync().HandleTask(); tunnelHandler_CurrentTunnelFunc = (_, _) => UpdatePingAsync().HandleTask(); + v3ConnectionState = new(tunnelHandler); + PostInitialize(); } @@ -321,8 +305,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; @@ -346,18 +330,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; @@ -373,18 +346,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) { @@ -407,10 +370,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(); } @@ -426,8 +389,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 @@ -458,7 +421,7 @@ protected override void CopyPlayerDataToUI() private void PrintTunnelServerInformation(string s) { - if (dynamicTunnelsEnabled) + if (v3ConnectionState.DynamicTunnelsEnabled) { AddNotice("Dynamic tunnels enabled".L10N("UI:Main:DynamicTunnelsEnabled")); } @@ -470,7 +433,11 @@ private void PrintTunnelServerInformation(string s) { AddNotice(string.Format(CultureInfo.CurrentCulture, "Current tunnel server: {0} {1} (Players: {2}/{3}) (Official: {4})".L10N("UI: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)); } } @@ -522,58 +489,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() @@ -678,13 +600,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() @@ -711,10 +627,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(); @@ -755,7 +671,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) @@ -806,8 +722,10 @@ private void Channel_MessageAdded(object sender, IRCMessageEventArgs e) { lbChatMessages.AddMessage(new ChatMessage( Color.Silver, - string.Format("Message blocked from {0}".L10N("UI:Main:MessageBlockedFromPlayer"), - e.Message.SenderName))); + string.Format( + CultureInfo.CurrentCulture, + "Message blocked from {0}".L10N("UI:Main:MessageBlockedFromPlayer"), + e.Message.SenderName))); } else { @@ -829,7 +747,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!"); @@ -854,7 +772,7 @@ private async ValueTask HostLaunchGameV2Async() ShowTunnelSelectionWindow(("An error occured while contacting " + "the CnCNet tunnel server." + Environment.NewLine + "Try picking a different tunnel server:").L10N("UI: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("UI:Main:ConnectTunnelError2"), CnCNetLobbyCommands.CHANGETUNNEL), ERROR_MESSAGE_COLOR); @@ -955,85 +873,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 @@ -1042,9 +895,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 @@ -1069,7 +922,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("UI:Main:PlayerConnectFailed"), playerName, CnCNetLobbyCommands.CHANGETUNNEL)); AbortGameStart(); } @@ -1099,9 +952,9 @@ private async ValueTask LaunchGameV3Async() Logger.Log("All players are connected, starting game!"); AddNotice("All players have connected...".L10N("UI: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); @@ -1115,7 +968,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(); @@ -1123,7 +976,7 @@ private async ValueTask LaunchGameV3Async() FindLocalPlayer().Port = gamePort; gameStartTimer.Pause(); - stunCancellationTokenSource?.Cancel(); + v3ConnectionState.StunCancellationTokenSource?.Cancel(); btnLaunchGame.InputEnabled = true; @@ -1135,7 +988,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; @@ -1143,7 +996,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); @@ -1339,49 +1192,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("UI: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("UI: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. @@ -1531,23 +1368,23 @@ protected override async ValueTask OnGameOptionChangedAsync() sb.Append(RandomSeed); sb.Append(Convert.ToInt32(RemoveStartingLocations)); sb.Append(Map.Name); - 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) { @@ -1557,12 +1394,6 @@ private async ValueTask ToggleP2PAsync() return; } - await CloseP2PPortsAsync(); - - internetGatewayDevice = null; - publicIpV4Address = null; - publicIpV6Address = null; - AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("UI:Main:P2PDisabled"), FindLocalPlayer().Name)); await SendPlayerP2PRequestAsync(); } @@ -1738,13 +1569,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("UI:Main:HostEnableDynamicTunnels"))); @@ -1808,8 +1639,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) @@ -2172,7 +2003,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); @@ -2183,8 +2014,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) @@ -2198,72 +2029,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("UI: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 { @@ -2273,48 +2055,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("UI:Main:PlayerP2PSupported"), playerName, p2pPlayer.LocalPingResults.Min(q => q.Ping))); @@ -2450,7 +2199,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("UI:Main:PlayerMissingMap"), sender, map.Name)); @@ -2462,7 +2211,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("UI:Main:UpdateMapToDBPrompt"), sender, map.Name)); @@ -2490,14 +2239,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("UI:Main:PlayerDownloadMapFailed") + " " + + string.Format(CultureInfo.CurrentCulture, "{0} has failed to download the map from the CnCNet map database.".L10N("UI:Main:PlayerDownloadMapFailed") + " " + "The host needs to change the map or {0} won't be able to participate in this match.".L10N("UI:Main:HostNeedChangeMapForPlayer"), sender)); } else { AddNotice( - string.Format("{0} has failed to download the map from the CnCNet map database.".L10N("UI:Main:PlayerDownloadMapFailed") + " " + + string.Format(CultureInfo.CurrentCulture, "{0} has failed to download the map from the CnCNet map database.".L10N("UI:Main:PlayerDownloadMapFailed") + " " + "You need to change the map or {0} won't be able to participate in this match.".L10N("UI:Main:YouNeedChangeMapForPlayer"), sender)); } @@ -2521,7 +2270,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("UI:Main:PlayerMissingMaDisabledSharing"), sender)); @@ -2578,6 +2327,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("UI:Main:DownloadMapCommandSha1AlreadyExists"), sha1, loadedMap.Map.BaseFilePath); @@ -2595,7 +2345,11 @@ private void DownloadMapByIdCommand(string parameters) chatCommandDownloadedMaps.Add(sha1); - message = string.Format("Attempting to download map via chat command: sha1={0}, mapName={1}".L10N("UI:Main:DownloadMapCommandStartingDownload"), sha1, mapName); + message = string.Format( + CultureInfo.CurrentCulture, + "Attempting to download map via chat command: sha1={0}, mapName={1}".L10N("UI: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 dc4195ef64206402771b9d049636e65e62b3ed52 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 13 Dec 2022 22:21:54 +0100 Subject: [PATCH 63/71] 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 0dabf7edb..2dd2d1c67 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 12409e04cc9f92e6c1b75ab5cce20a7fad6725b8 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 14 Dec 2022 17:36:41 +0100 Subject: [PATCH 64/71] Apply ConfigureAwait() --- ClientCore/Extensions/TaskExtensions.cs | 25 +- ClientCore/SavedGameManager.cs | 2 +- .../GameParsers/LogFileStatisticsParser.cs | 4 +- ClientGUI/GameProcessLogic.cs | 2 +- .../DXGUI/Generic/CampaignSelector.cs | 63 ++--- .../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, 730 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 191af0957..52a26209d 100644 --- a/ClientGUI/GameProcessLogic.cs +++ b/ClientGUI/GameProcessLogic.cs @@ -35,7 +35,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 faa6b95c2..b02591c8d 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,39 +263,42 @@ 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); - - // 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; - - spawnStreamWriter.WriteLine("CampaignID=" + mission.Index); - spawnStreamWriter.WriteLine("GameSpeed=" + UserINISettings.Instance.GameSpeed); - spawnStreamWriter.WriteLine("Firestorm=" + mission.RequiredAddon); - 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; + var spawnStreamWriter = new StreamWriter(SafePath.CombineFilePath(ProgramConstants.GamePath, ProgramConstants.SPAWNER_SETTINGS)); - spawnStreamWriter.WriteLine("DifficultyModeHuman=" + (mission.PlayerAlwaysOnNormalDifficulty ? "1" : trbDifficultySelector.Value.ToString())); - spawnStreamWriter.WriteLine("DifficultyModeComputer=" + GetComputerDifficulty()); + 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); + + // 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); + 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)); @@ -311,7 +314,7 @@ private async ValueTask LaunchMissionAsync(Mission mission) discordHandler.UpdatePresence(mission.GUIName, 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 95f987ef3..4821c5781 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 501763f3b..d94d1690c 100644 --- a/DXMainClient/DXGUI/Generic/MainMenu.cs +++ b/DXMainClient/DXGUI/Generic/MainMenu.cs @@ -530,7 +530,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); } } @@ -991,7 +991,7 @@ private async ValueTask ExitClientAsync() Logger.Log("Exiting."); WindowManager.CloseGame(); #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 b144d3d25..024b7ba69 100644 --- a/DXMainClient/DXGUI/Generic/StatisticsWindow.cs +++ b/DXMainClient/DXGUI/Generic/StatisticsWindow.cs @@ -509,7 +509,7 @@ private void LbGameList_SelectedIndexChanged(object sender, EventArgs e) XNAListBoxItem spectatorItem = new XNAListBoxItem(); spectatorItem.Text = "Spectator".L10N("UI: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 da3d085b8..120858c18 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 f7d34a23c..2c1896554 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("UI: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("UI: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("UI: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("UI: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("UI: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("UI: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("UI:Main:LoadGame"); diff --git a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs index 4ca386de3..3f75818e7 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/CnCNetLobby.cs @@ -600,9 +600,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); } } @@ -631,13 +631,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); } } @@ -793,7 +793,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) @@ -805,7 +805,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); } /// @@ -857,7 +857,7 @@ private async ValueTask JoinGameAsync(HostedCnCNetGame hg, string password } } - await JoinGameAsync(hg, password); + await JoinGameAsync(hg, password).ConfigureAwait(false); return true; } @@ -880,7 +880,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; @@ -889,13 +889,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) @@ -910,7 +910,7 @@ private async ValueTask OnGameLocked(object sender) SortAndRefreshHostedGames(); } - await ClearGameJoinAttemptAsync((Channel)sender); + await ClearGameJoinAttemptAsync((Channel)sender).ConfigureAwait(false); } private HostedCnCNetGame FindGameByChannelName(string channelName) @@ -925,7 +925,7 @@ private HostedCnCNetGame FindGameByChannelName(string channelName) private async ValueTask GameChannel_InvalidPasswordEntered_NewGameAsync(object sender) { connectionManager.MainChannel.AddMessage(new ChatMessage(Color.White, "Incorrect password!".L10N("UI:Main:PasswordWrong"))); - await ClearGameJoinAttemptAsync((Channel)sender); + await ClearGameJoinAttemptAsync((Channel)sender).ConfigureAwait(false); } private async ValueTask GameChannel_UserAddedAsync(object sender, ChannelUserEventArgs e) @@ -935,7 +935,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(); } @@ -944,7 +944,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) @@ -987,10 +987,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("UI:Main:CreateGameNamed"), e.GameRoomName))); @@ -1012,7 +1012,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("UI:Main:CreateGameNamed"), e.GameRoomName))); @@ -1027,7 +1027,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; } @@ -1040,7 +1040,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; } @@ -1070,7 +1070,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; } @@ -1123,13 +1123,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) { @@ -1140,7 +1140,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); } } @@ -1190,7 +1190,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; } @@ -1235,11 +1235,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("UI:Main:JoinFailedTitle"), @@ -1285,7 +1285,7 @@ private async ValueTask DdCurrentChannel_SelectedIndexChangedAsync() connectionManager.RemoveChannelFromUser(user.IRCUser.Name, currentChatChannel.ChannelName); }); - await currentChatChannel.LeaveAsync(); + await currentChatChannel.LeaveAsync().ConfigureAwait(false); } } @@ -1310,7 +1310,7 @@ private async ValueTask DdCurrentChannel_SelectedIndexChangedAsync() if (currentChatChannel.ChannelName != "#cncnet" && currentChatChannel.ChannelName != gameCollection.GetGameChatChannelNameFromIdentifier(localGameID)) { - await currentChatChannel.JoinAsync(); + await currentChatChannel.JoinAsync().ConfigureAwait(false); } } @@ -1541,7 +1541,7 @@ private async ValueTask BtnLogout_LeftClickAsync() if (connectionManager.IsConnected && !UserINISettings.Instance.PersistentMode) { - await connectionManager.DisconnectAsync(); + await connectionManager.DisconnectAsync().ConfigureAwait(false); } topBar.SwitchToPrimary(); @@ -1662,7 +1662,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 985ae3ba6..ffe24a57a 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 eb678df7c..e5297e587 100644 --- a/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs +++ b/DXMainClient/DXGUI/Multiplayer/CnCNet/PrivateMessagingWindow.cs @@ -529,7 +529,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 6c507ed80..574970c62 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); @@ -486,7 +490,7 @@ private async ValueTask DdSavedGame_SelectedIndexChangedAsync() CopyPlayerDataToUI(); if (!isSettingUp) - await BroadcastOptionsAsync(); + await BroadcastOptionsAsync().ConfigureAwait(false); UpdateDiscordPresence(); } @@ -495,7 +499,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 5c24c05bd..9bbb8f6b0 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -320,7 +320,7 @@ public async ValueTask SetUpAsync( { RandomSeed = new Random().Next(); - await RefreshMapSelectionUIAsync(); + await RefreshMapSelectionUIAsync().ConfigureAwait(false); btnChangeTunnel.Enable(); } else @@ -354,12 +354,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; @@ -368,7 +368,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(); @@ -381,7 +381,7 @@ await connectionManager.SendCustomMessageAsync(new( TopBar.SwitchToPrimary(); WindowManager.SelectedControl = tbChatInput; ResetAutoReadyCheckbox(); - await UpdatePingAsync(); + await UpdatePingAsync().ConfigureAwait(false); UpdateDiscordPresence(true); } @@ -396,7 +396,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(); @@ -449,8 +449,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) @@ -461,7 +461,7 @@ public void ChangeChatColor(IRCColor chatColor) public override async ValueTask ClearAsync() { - await base.ClearAsync(); + await base.ClearAsync().ConfigureAwait(false); if (channel != null) { @@ -503,16 +503,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(); } @@ -569,13 +569,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("UI:Main:HostAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); + await BtnLeaveGame_LeftClickAsync().ConfigureAwait(false); } else { @@ -589,7 +589,7 @@ private async ValueTask Channel_UserKickedAsync(UserNameEventArgs e) { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "You were kicked from the game!".L10N("UI:Main:YouWereKicked"))); - await ClearAsync(); + await ClearAsync().ConfigureAwait(false); Visible = false; Enabled = false; @@ -611,7 +611,7 @@ private async ValueTask Channel_UserListReceivedAsync() { connectionManager.MainChannel.AddMessage( new(ERROR_MESSAGE_COLOR, "The game host has abandoned the game.".L10N("UI:Main:HostHasAbandoned"))); - await BtnLeaveGame_LeftClickAsync(); + await BtnLeaveGame_LeftClickAsync().ConfigureAwait(false); } } @@ -648,9 +648,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 @@ -662,7 +662,7 @@ private async ValueTask Channel_UserAddedAsync(ChannelUserEventArgs e) if (Players.Count >= playerLimit) { AddNotice("Player limit reached. The game room has been locked.".L10N("UI:Main:GameRoomNumberLimitReached")); - await LockGameAsync(); + await LockGameAsync().ConfigureAwait(false); } } @@ -675,12 +675,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) @@ -746,9 +746,9 @@ protected override async ValueTask HostLaunchGameAsync() AddNotice("Contacting remote hosts...".L10N("UI: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!"); @@ -760,12 +760,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) { @@ -781,9 +781,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) @@ -810,7 +811,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; @@ -905,7 +907,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() @@ -915,7 +917,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); } @@ -944,7 +946,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() @@ -980,7 +982,7 @@ private async ValueTask LaunchGameV3Async() btnLaunchGame.InputEnabled = true; - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); } private void AbortGameStart() @@ -1027,7 +1029,8 @@ protected override async ValueTask RequestReadyStatusAsync() "you will be unable to participate in the match.").L10N("UI: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; } @@ -1040,7 +1043,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)); @@ -1106,7 +1110,7 @@ private async ValueTask HandleOptionsRequestAsync(string playerName, int options pInfo.TeamId = team; CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } /// @@ -1126,7 +1130,7 @@ private async ValueTask HandleReadyRequestAsync(string playerName, int readyStat pInfo.AutoReady = readyStatus > 1; CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } /// @@ -1177,8 +1181,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() @@ -1188,7 +1192,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() @@ -1200,7 +1205,7 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() try { - p2pSetupSucceeded = await v3ConnectionState.HandlePlayerP2PRequestAsync(); + p2pSetupSucceeded = await v3ConnectionState.HandlePlayerP2PRequestAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -1214,7 +1219,7 @@ private async ValueTask BroadcastPlayerP2PRequestAsync() } if (p2pSetupSucceeded) - await SendPlayerP2PRequestAsync(); + await SendPlayerP2PRequestAsync().ConfigureAwait(false); } private ValueTask SendPlayerP2PRequestAsync() @@ -1330,7 +1335,7 @@ private void ApplyPlayerOptions(string sender, string message) /// protected override async ValueTask OnGameOptionChangedAsync() { - await base.OnGameOptionChangedAsync(); + await base.OnGameOptionChangedAsync().ConfigureAwait(false); if (!IsHost) return; @@ -1370,32 +1375,32 @@ protected override async ValueTask OnGameOptionChangedAsync() sb.Append(Map.Name); 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("UI:Main:P2PEnabled"), FindLocalPlayer().Name)); - await BroadcastPlayerP2PRequestAsync(); + await BroadcastPlayerP2PRequestAsync().ConfigureAwait(false); return; } AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled P2P".L10N("UI:Main:P2PDisabled"), FindLocalPlayer().Name)); - await SendPlayerP2PRequestAsync(); + await SendPlayerP2PRequestAsync().ConfigureAwait(false); } /// @@ -1458,16 +1463,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 @@ -1570,7 +1575,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) @@ -1588,7 +1593,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); } } @@ -1604,7 +1609,7 @@ private async ValueTask RequestMapAsync() AddNotice("The game host has selected a map that doesn't exist on your installation.".L10N("UI: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("UI: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); } } @@ -1636,8 +1641,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(); @@ -1646,14 +1651,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); } } @@ -1705,7 +1710,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() @@ -1721,11 +1726,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) @@ -1769,78 +1774,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) @@ -1882,7 +1887,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); } } @@ -1899,7 +1904,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); } @@ -1908,14 +1913,14 @@ protected override async ValueTask HandleLockGameButtonClickAsync() if (!Locked) { AddNotice("You've locked the game room.".L10N("UI:Main:RoomLockedByYou")); - await LockGameAsync(); + await LockGameAsync().ConfigureAwait(false); } else { if (Players.Count < playerLimit) { AddNotice("You've unlocked the game room.".L10N("UI:Main:RoomUnockedByYou")); - await UnlockGameAsync(false); + await UnlockGameAsync(false).ConfigureAwait(false); } else { @@ -1927,7 +1932,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("UI:Main:UnlockGame"); @@ -1937,7 +1942,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; @@ -1956,7 +1961,7 @@ protected override async ValueTask KickPlayerAsync(int playerIndex) PlayerInfo pInfo = Players[playerIndex]; AddNotice(string.Format(CultureInfo.CurrentCulture, "Kicking {0} from the game...".L10N("UI: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) @@ -1970,8 +1975,8 @@ protected override async ValueTask BanPlayerAsync(int playerIndex) if (user != null) { AddNotice(string.Format(CultureInfo.CurrentCulture, "Banning and kicking {0} from the game...".L10N("UI: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); } } @@ -1996,7 +2001,7 @@ private async ValueTask HandleTunnelServerChangeMessageAsync(string sender, stri return; } - await HandleTunnelServerChangeAsync(tunnel); + await HandleTunnelServerChangeAsync(tunnel).ConfigureAwait(false); btnLaunchGame.AllowClick = true; } @@ -2040,12 +2045,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 { @@ -2088,7 +2094,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("UI: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; } @@ -2102,7 +2108,7 @@ private async ValueTask MapSharer_HandleMapDownloadFailedAsync(SHA1EventArgs e) } AddNotice("Requesting the game host to upload the map to the CnCNet map database.".L10N("UI: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) @@ -2120,7 +2126,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)) @@ -2137,7 +2143,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("UI: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); } } @@ -2151,7 +2157,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("UI: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); } } @@ -2162,7 +2168,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); } } @@ -2301,7 +2307,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) { @@ -2319,7 +2325,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); @@ -2409,7 +2415,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("UI:Main:GameLobby"); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index 642f4f36b..856d02328 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -389,7 +389,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("UI:Main:PresetLoaded")); else AddNotice(string.Format("Preset {0} not found!".L10N("UI:Main:PresetNotFound"), presetName)); @@ -408,7 +408,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) @@ -418,7 +418,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() @@ -442,7 +442,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) @@ -645,7 +645,7 @@ private async ValueTask DeleteSelectedMapAsync() } ListMaps(); - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); } catch (IOException ex) { @@ -659,7 +659,7 @@ private async ValueTask LbGameModeMapList_SelectedIndexChangedAsync() { if (lbGameModeMapList.SelectedIndex < 0 || lbGameModeMapList.SelectedIndex >= lbGameModeMapList.ItemCount) { - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); return; } @@ -667,7 +667,7 @@ private async ValueTask LbGameModeMapList_SelectedIndexChangedAsync() GameModeMap = (GameModeMap)item.Tag; - await ChangeMapAsync(GameModeMap); + await ChangeMapAsync(GameModeMap).ConfigureAwait(false); } private async ValueTask PickRandomMapAsync() @@ -683,7 +683,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(); @@ -713,7 +713,7 @@ protected async ValueTask RefreshMapSelectionUIAsync() return; if (ddGameModeMapFilter.SelectedIndex == gameModeMapFilterIndex) - await DdGameModeMapFilter_SelectedIndexChangedAsync(); + await DdGameModeMapFilter_SelectedIndexChangedAsync().ConfigureAwait(false); ddGameModeMapFilter.SelectedIndex = gameModeMapFilterIndex; } @@ -1648,7 +1648,7 @@ protected virtual async ValueTask StartGameAsync() GameProcessLogic.GameProcessExited += GameProcessExited_Callback; - await GameProcessLogic.StartGameProcessAsync(WindowManager); + await GameProcessLogic.StartGameProcessAsync(WindowManager).ConfigureAwait(false); UpdateDiscordPresence(true); } @@ -1706,10 +1706,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; } } @@ -2049,7 +2049,7 @@ protected virtual async ValueTask ChangeMapAsync(GameModeMap gameModeMap) pInfo.TeamId = 1; } - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); MapPreviewBox.GameModeMap = GameModeMap; CopyPlayerDataToUI(); @@ -2318,7 +2318,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 92bd585d4..ced855ef3 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("UI: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("UI:Main:MapNotExist") + "The host needs to change the map or you won't be able to play.".L10N("UI: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 fce7cac39..875e7fa4a 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 Localization; @@ -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 4348dd4f0..7ca316d0a 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -223,7 +223,7 @@ private async ValueTask FSWEventAsync(FileSystemEventArgs e) gameSaved = true; - await SavedGameManager.RenameSavedGameAsync(); + await SavedGameManager.RenameSavedGameAsync().ConfigureAwait(false); } } @@ -249,16 +249,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); } } @@ -285,9 +285,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(); @@ -346,14 +346,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() { btnLaunchGame.Enabled = !chkAutoReady.Checked; - await RequestReadyStatusAsync(); + await RequestReadyStatusAsync().ConfigureAwait(false); } protected void ResetAutoReadyCheckbox() @@ -377,7 +377,7 @@ private async ValueTask SetFrameSendRateAsync(string value) FrameSendRate = intValue; AddNotice(string.Format("FrameSendRate has been changed to {0}".L10N("UI:Main:FrameSendRateChanged"), intValue)); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); ClearReadyStatuses(); } @@ -395,7 +395,7 @@ private async ValueTask SetMaxAheadAsync(string value) MaxAhead = intValue; AddNotice(string.Format("MaxAhead has been changed to {0}".L10N("UI:Main:MaxAheadChanged"), intValue)); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); } @@ -418,7 +418,7 @@ private async ValueTask SetProtocolVersionAsync(string value) ProtocolVersion = intValue; AddNotice(string.Format("ProtocolVersion has been changed to {0}".L10N("UI:Main:ProtocolVersionChanged"), intValue)); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); } @@ -428,7 +428,7 @@ private async ValueTask SetStartingLocationClearanceAsync(string value) SetRandomStartingLocations(removeStartingLocations); - await OnGameOptionChangedAsync(); + await OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); } @@ -490,7 +490,7 @@ private async ValueTask RollDiceCommandAsync(string dieType) results[i] = random.Next(1, dieSides + 1); } - await BroadcastDiceRollAsync(dieSides, results); + await BroadcastDiceRollAsync(dieSides, results).ConfigureAwait(false); } /// @@ -708,7 +708,7 @@ private async ValueTask MapPreviewBox_StartingLocationAppliedAsync() { ClearReadyStatuses(); CopyPlayerDataToUI(); - await BroadcastPlayerOptionsAsync(); + await BroadcastPlayerOptionsAsync().ConfigureAwait(false); } /// @@ -721,13 +721,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; } @@ -743,7 +743,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() { if (occupiedColorIds.Contains(player.ColorId) && player.ColorId > 0) { - await SharedColorsNotificationAsync(); + await SharedColorsNotificationAsync().ConfigureAwait(false); return; } @@ -752,7 +752,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; } @@ -767,7 +767,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() p => p.StartingLocation == pInfo.StartingLocation && p.Name != pInfo.Name) != null) { - await SharedStartingLocationNotificationAsync(); + await SharedStartingLocationNotificationAsync().ConfigureAwait(false); return; } } @@ -783,7 +783,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() if (index > -1 && index != aiId) { - await SharedStartingLocationNotificationAsync(); + await SharedStartingLocationNotificationAsync().ConfigureAwait(false); return; } } @@ -794,13 +794,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; } } @@ -815,43 +815,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() @@ -943,7 +924,7 @@ public virtual ValueTask ClearAsync() protected override async ValueTask OnGameOptionChangedAsync() { - await base.OnGameOptionChangedAsync(); + await base.OnGameOptionChangedAsync().ConfigureAwait(false); ClearReadyStatuses(); CopyPlayerDataToUI(); @@ -958,8 +939,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; } @@ -973,7 +954,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() @@ -994,11 +975,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); @@ -1009,20 +985,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]); } @@ -1098,14 +1062,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 e590a8694..fb38da91b 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/SkirmishLobby.cs @@ -139,7 +139,7 @@ private string CheckGameValidity() Map.MaxPlayers); } - IEnumerable concatList = Players.Concat(AIPlayers); + List concatList = Players.Concat(AIPlayers).ToList(); foreach (PlayerInfo pInfo in concatList) { @@ -172,7 +172,7 @@ protected override async ValueTask BtnLaunchGame_LeftClickAsync() if (error == null) { SaveSettings(); - await StartGameAsync(); + await StartGameAsync().ConfigureAwait(false); return; } @@ -229,8 +229,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 99f13b1ba..b401c4013 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 85a22fc38..1ba8bf034 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 226f3fadd..8afaf22a5 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -8,6 +8,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; @@ -260,8 +261,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); @@ -379,7 +382,7 @@ public bool SetInfoFromMpMapsINI(IniFile iniFile) #if !GL if (UserINISettings.Instance.PreloadMapPreviews) - PreviewTexture = LoadPreviewTexture(); + PreviewTexture = await LoadPreviewTextureAsync().ConfigureAwait(false); #endif // Parse forced options @@ -652,7 +655,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); @@ -660,7 +663,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 c74140875..0a3004c53 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -54,14 +54,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); @@ -87,7 +87,7 @@ private void LoadMultiMaps(IniFile mpMapsIni) Map map = new Map(mapFilePathValue); - if (!map.SetInfoFromMpMapsINI(mpMapsIni)) + if (!await map.SetInfoFromMpMapsINIAsync(mpMapsIni).ConfigureAwait(false)) continue; maps.Add(map); @@ -141,7 +141,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(); @@ -162,7 +162,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))) @@ -203,12 +203,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.CalculateSHA(); 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 f1ce50d55..f07339a7b 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 fa5378636..3e84319ef 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("UI: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("UI: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 206a86ae5..759475246 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 f7baffb73..777a398e5 100644 --- a/DXMainClient/Startup.cs +++ b/DXMainClient/Startup.cs @@ -243,11 +243,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()) { @@ -263,7 +261,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()) { @@ -285,7 +283,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()) @@ -315,23 +313,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); @@ -365,7 +363,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 13fd239692ee8c34dc2251320dd791e022b32782 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 18 Dec 2022 03:09:56 +0100 Subject: [PATCH 65/71] 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 6b7403b39..a95687b5a 100644 --- a/ClientCore/ClientConfiguration.cs +++ b/ClientCore/ClientConfiguration.cs @@ -10,7 +10,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 CLIENT_SETTINGS = "DTACnCNetClient.ini"; private const string GAME_OPTIONS = "GameOptions.ini"; diff --git a/ClientCore/ProgramConstants.cs b/ClientCore/ProgramConstants.cs index b86d274ee..0a04bfd53 100644 --- a/ClientCore/ProgramConstants.cs +++ b/ClientCore/ProgramConstants.cs @@ -47,6 +47,7 @@ public static class ProgramConstants public const string SPAWNER_SETTINGS = "spawn.ini"; public const string SAVED_GAME_SPAWN_INI = SAVED_GAMES_DIRECTORY + "/spawnSG.ini"; public const string SAVED_GAMES_DIRECTORY = "Saved Games"; + 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 97a7bd16b..5d45d01b3 100644 --- a/ClientCore/Settings/UserINISettings.cs +++ b/ClientCore/Settings/UserINISettings.cs @@ -101,6 +101,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); @@ -277,6 +278,8 @@ protected UserINISettings(IniFile iniFile) public BoolSetting AutoRemoveUnderscoresFromName { 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 2dd2d1c67..e2c28f407 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; @@ -94,7 +94,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)) @@ -109,7 +109,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); } @@ -177,8 +177,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 46f8810b6..ba8d2eb02 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 024b7ba69..887249016 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 { @@ -149,7 +151,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; @@ -200,9 +202,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"; @@ -383,7 +385,7 @@ public override void Initialize() panelTotalStatistics.AddChild(lblFavouriteSideValue); panelTotalStatistics.AddChild(lblAverageAILevelValue); -#endregion + #endregion AddChild(tabControl); AddChild(lblFilter); @@ -407,7 +409,7 @@ public override void Initialize() mpColors = MultiplayerColor.LoadColors(); - ReadStatistics(); + ReadStatisticsAsync().HandleTask(); ListGameModes(); ListGames(); @@ -514,7 +516,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 @@ -573,12 +575,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() { @@ -995,10 +993,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(); } @@ -1019,12 +1017,10 @@ private void BtnClearStatistics_LeftClick(object sender, EventArgs e) Environment.NewLine + Environment.NewLine + "Are you sure you want to continue?").L10N("UI: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 574970c62..da9dbbab3 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 9bbb8f6b0..abdc3d7fb 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -102,12 +102,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()), @@ -118,7 +117,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()), @@ -148,6 +147,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, @@ -174,8 +174,16 @@ public CnCNetGameLobby( "Toggle P2P connections on/off, your IP will be public to players in the lobby".L10N("UI:Main:ChangeP2P"), false, _ => ToggleP2PAsync().HandleTask())); - - WindowManager.GameClosing += (_, _) => Dispose(true); + AddChatBoxCommand(new( + CnCNetLobbyCommands.RECORD, + "Toggle recording game replay".L10N("UI:Main:ChangeRecord"), + false, + _ => ToggleRecord())); + AddChatBoxCommand(new( + CnCNetLobbyCommands.REPLAY, + "Start a game replay.\nExample: \"/replay REPLAYID".L10N("UI:Main:StartReplay"), + true, + StartReplay)); } public event EventHandler GameLeft; @@ -268,7 +276,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; @@ -276,7 +284,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) @@ -307,6 +315,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; @@ -668,7 +677,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); @@ -883,6 +892,7 @@ private void StartV3ConnectionListeners() void RemoteHostConnectionFailedAction() => AddCallback(() => GameTunnelHandler_ConnectionFailed_CallbackAsync().HandleTask()); v3ConnectionState.StartV3ConnectionListeners( + UniqueGameID, gameLocalPlayerId, FindLocalPlayer().Name, Players, @@ -918,15 +928,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("UI:Main:PlayerConnectFailed"), playerName, CnCNetLobbyCommands.CHANGETUNNEL)); - AbortGameStart(); + await AbortGameStartAsync().ConfigureAwait(false); } private async ValueTask HandlePlayerConnectedToTunnelAsync(string playerName) @@ -939,7 +949,7 @@ private async ValueTask HandlePlayerConnectedToTunnelAsync(string playerName) if (index == -1) { Logger.Log("HandleTunnelConnected: Couldn't find player " + playerName + "!"); - AbortGameStart(); + await AbortGameStartAsync().ConfigureAwait(false); return; } @@ -954,25 +964,7 @@ private async ValueTask LaunchGameV3Async() Logger.Log("All players are connected, starting game!"); AddNotice("All players have connected...".L10N("UI: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; @@ -985,12 +977,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; @@ -1214,7 +1207,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("UI:Main:UPnPP2PFailed")), Color.Orange); - + await SendPlayerP2PRequestAsync().ConfigureAwait(false); return; } @@ -1403,6 +1396,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("UI:Main:RecordEnabled"), FindLocalPlayer().Name)); + else + AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled game recording".L10N("UI:Main:RecordDisabled"), FindLocalPlayer().Name)); + } + + private void StartReplay(string replayId) + { + } + /// /// Handles game option messages received from the game host. /// @@ -1589,9 +1596,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); } @@ -1644,8 +1649,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) @@ -2011,31 +2016,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("UI: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("UI:Main:TunnelNegotiated"), playerName, tunnel.Name, tunnel.PingInMs)); } } @@ -2402,8 +2392,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 856d02328..d27986706 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1654,19 +1654,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; } /// @@ -2049,7 +2047,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 875e7fa4a..0613283e4 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 7ca316d0a..44ce1ebc6 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/MultiplayerGameLobby.cs @@ -249,7 +249,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 8afaf22a5..43a4bd87c 100644 --- a/DXMainClient/Domain/Multiplayer/Map.cs +++ b/DXMainClient/Domain/Multiplayer/Map.cs @@ -663,7 +663,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 0a3004c53..c6ebfc0bd 100644 --- a/DXMainClient/Domain/Multiplayer/MapLoader.cs +++ b/DXMainClient/Domain/Multiplayer/MapLoader.cs @@ -171,7 +171,7 @@ private async ValueTask LoadCustomMapsAsync() } // save cache - CacheCustomMaps(customMapCache); + await CacheCustomMapsAsync(customMapCache).ConfigureAwait(false); foreach (Map map in customMapCache.Values) { @@ -183,16 +183,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 5a1ca8bba1adcd60a727ffe5388b1cc03a1ffad0 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Sun, 18 Dec 2022 15:42:48 +0100 Subject: [PATCH 66/71] 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 abdc3d7fb..1547b43a1 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -178,7 +178,7 @@ public CnCNetGameLobby( CnCNetLobbyCommands.RECORD, "Toggle recording game replay".L10N("UI:Main:ChangeRecord"), false, - _ => ToggleRecord())); + _ => ToggleRecordAsync().HandleTask())); AddChatBoxCommand(new( CnCNetLobbyCommands.REPLAY, "Start a game replay.\nExample: \"/replay REPLAYID".L10N("UI:Main:StartReplay"), @@ -1396,11 +1396,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("UI:Main:RecordEnabled"), FindLocalPlayer().Name)); else AddNotice(string.Format(CultureInfo.CurrentCulture, "Player {0} disabled game recording".L10N("UI:Main:RecordDisabled"), FindLocalPlayer().Name)); diff --git a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs index d27986706..903458e53 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1659,9 +1659,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 7cc06e4dbdeae916829ec4cca05d65a5b3342c11 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Tue, 20 Dec 2022 11:33:15 +0100 Subject: [PATCH 67/71] 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 903458e53..88c459439 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/GameLobbyBase.cs @@ -1654,17 +1654,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 44ce1ebc6..2f84d3ec7 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 11af8fb625ed9a261450fc2180b0656333509330 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 21 Dec 2022 13:35:08 +0100 Subject: [PATCH 68/71] 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 759475246..f99ebaa0d 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 0aa7feca9c6b1aa42783802afcb5d14ddff225ff Mon Sep 17 00:00:00 2001 From: Grant Bartlett Date: Wed, 21 Dec 2022 20:26:02 +0000 Subject: [PATCH 69/71] Quakenet test Branches from networkstack branch --- .../Domain/Multiplayer/CnCNet/IRCChannelModes.cs | 5 ++++- DXMainClient/Online/Connection.cs | 10 +--------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs b/DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs index b11fa6b0a..6aeb6fef7 100644 --- a/DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs +++ b/DXMainClient/Domain/Multiplayer/CnCNet/IRCChannelModes.cs @@ -10,5 +10,8 @@ internal static class IRCChannelModes 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}"; + public const char NO_CTCP = 'C'; + public const char TOPIC_LIMIT = 't'; + public const char NO_NICK_CHANGE = 'N'; + public static string DEFAULT = $"{CHANNEL_KEY}{CHANNEL_LIMIT}{NO_EXTERNAL_MESSAGES}{NO_NICKNAME_CHANGE}{SECRET_CHANNEL}-{NO_CTCP}{TOPIC_LIMIT}{NO_NICK_CHANGE}"; } \ No newline at end of file diff --git a/DXMainClient/Online/Connection.cs b/DXMainClient/Online/Connection.cs index f99ebaa0d..526b98755 100644 --- a/DXMainClient/Online/Connection.cs +++ b/DXMainClient/Online/Connection.cs @@ -42,15 +42,7 @@ 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("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("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("irc.gamesurge.net", "GameSurge", new[] { 6667 }) + new("irc.quakenet.org", "QuakeNet", new[] { 6667 }) }.AsReadOnly(); private bool IsConnected { get; set; } From 527da28036d5bcdff33652a3942faf28534e73a1 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 21 Dec 2022 22:07:15 +0100 Subject: [PATCH 70/71] 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 8a8279f6ae16cb6a980eece87a011a10c56a42b4 Mon Sep 17 00:00:00 2001 From: Rans4ckeR Date: Wed, 21 Dec 2022 23:38:33 +0100 Subject: [PATCH 71/71] 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 1547b43a1..3e474d267 100644 --- a/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs +++ b/DXMainClient/DXGUI/Multiplayer/GameLobby/CnCNetGameLobby.cs @@ -174,6 +174,7 @@ public CnCNetGameLobby( "Toggle P2P connections on/off, your IP will be public to players in the lobby".L10N("UI:Main:ChangeP2P"), false, _ => ToggleP2PAsync().HandleTask())); +#if DEBUG AddChatBoxCommand(new( CnCNetLobbyCommands.RECORD, "Toggle recording game replay".L10N("UI:Main:ChangeRecord"), @@ -184,6 +185,7 @@ public CnCNetGameLobby( "Start a game replay.\nExample: \"/replay REPLAYID".L10N("UI: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) {