using LobbyServerDto; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Net; 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); 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, string? ip, int port) { 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, HostIp = ip, HostPort = 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); }); } public delegate void SendUdpMessageCallback(IPEndPoint remoteEndpoint, byte[] messageBuffer, int messageLength); public void RequestLobbyNatPunch(Guid lobbyId, string? password, SendUdpMessageCallback sendUdpCallback) { byte[]? passwordHash = null; if (!string.IsNullOrEmpty(password) && lobbyInformation.ContainsKey(lobbyId) && lobbyInformation[lobbyId].PasswordSalt != null) passwordHash = PasswordHash.Hash(password, lobbyInformation[lobbyId].PasswordSalt!); Task.Run(() => { QueryExternalIpAndPort(sendUdpCallback); 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, string? ip, 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, HostIp = ip, HostPort = 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() { 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 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 void Dispose() { waitForExternalIp.Dispose(); tcpClient.Dispose(); } } }