using LobbyServerDto; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; namespace Lobbies { public class LobbyClient : IDisposable { TcpLobbbyClient tcpClient = new TcpLobbbyClient(); private readonly ConcurrentQueue events = new ConcurrentQueue(); BufferRental bufferRental = new BufferRental(4096); AutoResetEvent waitForExternalIp = new AutoResetEvent(false); UdpEchoServer udpEchoServer = new UdpEchoServer(); private Dictionary lobbyInformation = new Dictionary(); private string? host; private int port; private int connectionId; public string? externalIp; public int externalPort; public void Connect(string host, int port, CancellationToken cancellationToken) { this.host = host; this.port = port; try { cancellationToken.ThrowIfCancellationRequested(); using var cts = cancellationToken.Register(() => { tcpClient.Stop(); }); tcpClient.DataReceived -= TcpClient_DataReceived; tcpClient.DataReceived += TcpClient_DataReceived; tcpClient.Disconnected -= TcpClient_Disconnected; tcpClient.Disconnected += TcpClient_Disconnected; _ = Task.Run(() => tcpClient.Connect(host, port)); } catch { } } public IEnumerable ReadEvents(int maxEvents) { if (events.Count > 0) { maxEvents = Math.Min(maxEvents, events.Count); while (maxEvents > 0) { if(events.TryDequeue(out var _event)) { yield return _event; } else { break; } } } } public void HostLobby(Guid gameId, string name, int gameMode, int maxPlayerCount, string? password, int port) { udpEchoServer.Start(0); byte[]? hash = null, salt = null; if(!string.IsNullOrEmpty(password)) { (hash, salt) = PasswordHash.Hash(password); } var lobbyCreate = new LobbyCreate() { GameId = gameId, Name = name, GameMode = gameMode, MaxPlayerCount = maxPlayerCount, PlayerCount = 0, PasswordHash = hash, PasswordSalt = salt, HostIps = GatherLocalIpAddresses().ToArray(), HostPort = port, HostTryPort = udpEchoServer.Port }; byte[] messageData = bufferRental.Rent(); var len = lobbyCreate.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } public void RequestLobbyHostInfo(Guid lobbyId, string? password) { byte[]? passwordHash = null; if(!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null) passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!); var lobbyCreate = new LobbyRequestHostInfo() { LobbyId = lobbyId, PasswordHash = passwordHash, }; byte[] messageData = bufferRental.Rent(); var len = lobbyCreate.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } /// /// This callback is called if the nat punch requires the game server/client udp sockets sends a packet to a server so that /// the external mapped port by the firewall can be seen by the external point. /// /// The endpoint to send data to /// The message byte buffer to send /// Length of the data to send public delegate void SendUdpMessageCallback(IPEndPoint remoteEndpoint, byte[] messageBuffer, int messageLength); /// /// This function requests a nat punch for this client from the game host. First the external port for our game client is detected by /// sending a udp packet to the lobby server who then forwards the nat punch request to the host with the seen external ip and port. /// The game host then sends a udp packet to the client to open it's nat bridge and when done sends a event to the client via the lobby server. /// The client receives the external port and ip seen by the lobby server for the host and can connect to the host. /// /// The lobby to request a nat punch for /// Optional password of lobby /// A callback to send udp data if you have a udp game client ready and bound public void RequestLobbyNatPunch(Guid lobbyId, string? password, SendUdpMessageCallback sendUdpToGetExternalPortMappingCallback) { byte[]? passwordHash = null; if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null) passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!); Task.Run(() => { QueryExternalIpAndPort(sendUdpToGetExternalPortMappingCallback); var lobbyRequestNatPunch = new LobbyRequestNatPunch() { LobbyId = lobbyId, PasswordHash = passwordHash, ClientIp = externalIp, ClientPort = externalPort }; byte[] messageData = bufferRental.Rent(); var len = lobbyRequestNatPunch.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); }); } /// /// This function requests a nat punch for this client from the game host. First the external port for our game client is detected by /// sending a udp packet to the lobby server who then forwards the nat punch request to the host with the seen external ip and port. /// The game host then sends a udp packet to the client to open it's nat bridge and when done sends a event to the client via the lobby server. /// The client receives the external port and ip seen by the lobby server for the host and can connect to the host. /// /// The lobby to request a nat punch for /// Optional password of lobby /// The port the game client will later use. We create a udp socket on it and send a packet to the lobby server to get the firewall to map that port for a short period of time /// after that the udp client will be disposed public void RequestLobbyNatPunch(Guid lobbyId, string? password, int port = 0) { byte[]? passwordHash = null; if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null) passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!); Task.Run(() => { using (var udpEchoServer = new UdpEchoServer()) { udpEchoServer.Start(port); QueryExternalIpAndPort(udpEchoServer.Send); } var lobbyRequestNatPunch = new LobbyRequestNatPunch() { LobbyId = lobbyId, PasswordHash = passwordHash, ClientIp = externalIp, ClientPort = externalPort }; byte[] messageData = bufferRental.Rent(); var len = lobbyRequestNatPunch.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); }); } public void UpdateLobby(string name, int gameMode, int maxPlayerCount, int playerCount, string? password, int port) { byte[]? hash = null, salt = null; if (!string.IsNullOrEmpty(password)) { (hash, salt) = PasswordHash.Hash(password); } var lobbyUpdate = new LobbyUpdate() { Name = name, GameMode = gameMode, MaxPlayerCount = maxPlayerCount, PlayerCount = playerCount, PasswordHash = hash, PasswordSalt = salt, HostIps = GatherLocalIpAddresses().ToArray(), HostPort = port, HostTryPort = udpEchoServer.Port }; byte[] messageData = bufferRental.Rent(); var len = lobbyUpdate.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } public void CloseLobby() { udpEchoServer.Stop(); var lobbyDelete = new LobbyDelete() { }; byte[] messageData = bufferRental.Rent(); var len = lobbyDelete.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } public void ObserveLobbies(Guid gameId) { var lobbiesObserve = new LobbiesObserve() { GameId = gameId }; byte[] messageData = bufferRental.Rent(); var len = lobbiesObserve.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } public void StopObservingLobbies() { var lobbiesStopObserve = new LobbiesStopObserve() { }; byte[] messageData = bufferRental.Rent(); var len = lobbiesStopObserve.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } public void NotifyLobbyNatPunchDone(int natPunchId, string externalIp, int externalPort) { var lobbyNatPunchDone = new LobbyNatPunchDone() { NatPunchId = natPunchId, ExternalIp = externalIp, ExternalPort = externalPort }; byte[] messageData = bufferRental.Rent(); var len = lobbyNatPunchDone.Serialize(messageData); _ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); }); } public Task TryDirectConnection(IPAddress[] ipAddressesToTry, int tryPort) { return Task.Run(() => { IPAddress? ret = null; using(var waitForIpEvent = new AutoResetEvent(false)) using (var udpEchoClient = new UdpEchoServer()) { udpEchoClient.Reached += (ep) => { ret = ep.Address; try { waitForIpEvent.Set(); udpEchoClient.Stop(); } catch { } }; udpEchoClient.Start(0); foreach (var ip in ipAddressesToTry) { udpEchoClient.CheckConnectionPossible(new IPEndPoint(ip, tryPort)); } waitForIpEvent.WaitOne(500); } events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.DirectConnectionTestComplete, EventData = new DirectConnectionTestResult { DirectConnectionPossible = ret != null, IPAddress = ret } }); }); } public static IPAddress[] GetIPsByName(string hostName, bool ip4Wanted, bool ip6Wanted) { // Check if the hostname is already an IPAddress IPAddress? outIpAddress; if (IPAddress.TryParse(hostName, out outIpAddress) == true) return new IPAddress[] { outIpAddress }; //<---------- IPAddress[] addresslist = Dns.GetHostAddresses(hostName); if (addresslist == null || addresslist.Length == 0) return new IPAddress[0]; //<---------- if (ip4Wanted && ip6Wanted) return addresslist; //<---------- if (ip4Wanted) return addresslist.Where(o => o.AddressFamily == AddressFamily.InterNetwork).ToArray(); //<---------- if (ip6Wanted) return addresslist.Where(o => o.AddressFamily == AddressFamily.InterNetworkV6).ToArray(); //<---------- return new IPAddress[0]; } public void QueryExternalIpAndPort(SendUdpMessageCallback sendUdpCallback) { byte[] messageData = bufferRental.Rent(); try { waitForExternalIp.Reset(); var queryExternalPortAndIp = new QueryExternalPortAndIp() { LobbyClientId = connectionId }; var len = queryExternalPortAndIp.Serialize(messageData); var ip = GetIPsByName(host!, true, false).First(); do { sendUdpCallback(new IPEndPoint(ip, port), messageData, len); } while (!waitForExternalIp.WaitOne(100)); } catch { } finally { bufferRental.Return(messageData); } } public void Stop() { waitForExternalIp.Set(); tcpClient.Stop(); } private void TcpClient_Disconnected(bool clean, string error) { if(!clean) events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Failed, EventData = new LobbyClientDisconnectReason { WasError = true, ErrorMessage = error } }); else events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Disconnected, EventData = new LobbyClientDisconnectReason { WasError = false, ErrorMessage = string.Empty } }); } private void TcpClient_DataReceived(int dataLength, Memory data) { try { if (dataLength > 0) { switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(data.Span)) { case LobbyClientConnectionInfo.TypeId: { var lobbyClientConnectionInfo = LobbyClientConnectionInfo.Deserialize(data.Span); if (lobbyClientConnectionInfo != null) { connectionId = lobbyClientConnectionInfo.Id; events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Connected, EventData = null }); } } break; case LobbyInfo.TypeId: { var lobbyInfo = LobbyInfo.Deserialize(data.Span); if (lobbyInfo != null) { lobbyInformation[lobbyInfo.Id] = lobbyInfo; events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyUpdate, EventData = lobbyInfo }); } } break; case LobbyDelete.TypeId: { var lobbyDelete = LobbyDelete.Deserialize(data.Span); if (lobbyDelete != null) { lobbyInformation.Remove(lobbyDelete.Id); events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyDelete, EventData = lobbyDelete }); } } break; case LobbyHostInfo.TypeId: { var lobbyHostInfo = LobbyHostInfo.Deserialize(data.Span); if (lobbyHostInfo != null) { events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyHostInfo, EventData = lobbyHostInfo }); } } break; case LobbyRequestNatPunch.TypeId: { var lobbyRequestNatPunch = LobbyRequestNatPunch.Deserialize(data.Span); if (lobbyRequestNatPunch != null) { events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyRequestNatPunch, EventData = lobbyRequestNatPunch }); } } break; case LobbyNatPunchDone.TypeId: { var lobbyNatPunchDone = LobbyNatPunchDone.Deserialize(data.Span); if (lobbyNatPunchDone != null) { events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyNatPunchDone, EventData = lobbyNatPunchDone }); } } break; case SeenExternalIpAndPort.TypeId: { var seenExternalIpAndPort = SeenExternalIpAndPort.Deserialize(data.Span); if (seenExternalIpAndPort != null) { externalIp = seenExternalIpAndPort.Ip; externalPort = seenExternalIpAndPort.Port; waitForExternalIp.Set(); events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.ExternalIpAndPort, EventData = seenExternalIpAndPort }); } } break; } } } catch { } } public IEnumerable GatherLocalIpAddresses() { foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces()) { IPInterfaceProperties ipProps = netInterface.GetIPProperties(); foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses) { yield return addr.Address; } } } public void Dispose() { waitForExternalIp.Dispose(); tcpClient.Dispose(); udpEchoServer.Dispose(); } } }