main
Thomas Woischnig 2023-11-29 20:28:43 +01:00
parent 2674eb2a10
commit 840218f119
25 changed files with 650 additions and 169 deletions

View File

@ -2,6 +2,9 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@ -14,8 +17,21 @@ namespace Lobbies
TcpLobbbyClient tcpClient = new TcpLobbbyClient();
private readonly ConcurrentQueue<LobbyClientEvent> events = new ConcurrentQueue<LobbyClientEvent>();
BufferRental bufferRental = new BufferRental(4096);
AutoResetEvent waitForExternalIp = new AutoResetEvent(false);
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();
@ -30,9 +46,6 @@ namespace Lobbies
tcpClient.Disconnected -= TcpClient_Disconnected;
tcpClient.Disconnected += TcpClient_Disconnected;
tcpClient.Connected -= TcpClient_Connected;
tcpClient.Connected += TcpClient_Connected;
_ = Task.Run(() => tcpClient.Connect(host, port));
}
catch
@ -93,19 +106,25 @@ namespace Lobbies
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void RequestLobbyNatPunch(Guid lobbyId, string? password, string? clientIp, int clientPort)
public delegate void SendUdpMessageCallback(IPEndPoint remoteEndpoint, byte[] messageBuffer, int messageLength);
public void RequestLobbyNatPunch(Guid lobbyId, string? password, SendUdpMessageCallback sendUdpCallback)
{
Task.Run(() =>
{
QueryExternalIpAndPort(sendUdpCallback);
var lobbyRequestNatPunch = new LobbyRequestNatPunch()
{
LobbyId = lobbyId,
PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)),
ClientIp = clientIp,
ClientPort = clientPort
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)
@ -156,23 +175,75 @@ namespace Lobbies
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void NotifyLobbyNatPunchDone(int natPunchId)
public void NotifyLobbyNatPunchDone(int natPunchId, string externalIp, int externalPort)
{
var lobbyNatPunchDone = new LobbyNatPunchDone() { NatPunchId = natPunchId };
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 void Stop()
public static IPAddress[] GetIPsByName(string hostName, bool ip4Wanted, bool ip6Wanted)
{
tcpClient.Stop();
// 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];
}
private void TcpClient_Connected()
public void QueryExternalIpAndPort(SendUdpMessageCallback sendUdpCallback)
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Connected, EventData = null });
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)
@ -183,38 +254,24 @@ namespace Lobbies
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Disconnected, EventData = new LobbyClientDisconnectReason { WasError = false, ErrorMessage = string.Empty } });
}
private int PeekTypeId(ReadOnlySpan<byte> buffer)
{
int typeId = 0;
int shift = 0;
int offset = 0;
byte b;
do
{
{
// Check for a corrupted stream. Read a max of 5 bytes.
// In a future version, add a DataFormatException.
if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7
throw new FormatException("Format_Bad7BitInt32");
// ReadByte handles end of stream cases for us.
b = buffer[offset++];
typeId |= (b & 0x7F) << shift;
shift += 7;
}
} while ((b & 0x80) != 0);
return typeId;
}
private void TcpClient_DataReceived(int dataLength, Memory<byte> data)
{
try
{
if (dataLength > 0)
{
switch (PeekTypeId(data.Span))
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);
@ -260,6 +317,19 @@ namespace Lobbies
}
}
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;
}
}
}
@ -268,6 +338,7 @@ namespace Lobbies
public void Dispose()
{
waitForExternalIp.Dispose();
tcpClient.Dispose();
}

View File

@ -1,10 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lobbies
namespace Lobbies
{
public class LobbyClientDisconnectReason
{

View File

@ -1,27 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lobbies
namespace Lobbies
{
/// <summary>
/// Contains a lobby client event
/// </summary>
public class LobbyClientEvent
{
/// <summary>
/// Identifier type of the event
/// </summary>
public LobbyClientEventTypes EventType { get; internal set; }
/// <summary>
/// Optional data object, contains additional information for the event. See <see cref="LobbyClientEventTypes"/> for possible values.
/// </summary>
public object? EventData { get; internal set; }
}
/// <summary>
/// The list of events that can occur.
/// </summary>
public enum LobbyClientEventTypes
{
/// <summary>
/// Client connected to lobby server. EventData is null.
/// </summary>
Connected,
/// <summary>
/// Client disconnected from lobby server graceful. EventData is <see cref="LobbyClientDisconnectReason"/>.
/// </summary>
Disconnected,
/// <summary>
/// Connection lost to lobby server. EventData is LobbyClientDisconnectReason.
/// </summary>
Failed,
/// <summary>
/// A lobby was added to the lobby list for the observed game id. EventData is <see cref="LobbyAdd"/> with the lobby data.
/// </summary>
LobbyAdd,
/// <summary>
/// A lobby was changed for the observed game id. EventData is <see cref="LobbyUpdate"/> with the lobby data.
/// </summary>
LobbyUpdate,
/// <summary>
/// A lobby was removed from the lobby list for the observed game id. EventData is <see cref="LobbyDelete"/> with the lobby id.
/// </summary>
LobbyDelete,
/// <summary>
/// The game host shared his self known address. EventData is <see cref="LobbyHostInfo"/> with the games hosts self known address, most likely internal.
/// </summary>
LobbyHostInfo,
/// <summary>
/// A nat punch was requested by another LobbyClient to be performed by the host LobbyClient. EventData is <see cref="LobbyRequestNatPunch"/> with the clients seen external address to nat punch to.
/// </summary>
LobbyRequestNatPunch,
/// <summary>
/// Game host has finished the nat punch. EventData is <see cref="LobbyRequestNatPunch"/> with the games hosts external address.
/// </summary>
LobbyNatPunchDone,
/// <summary>
/// Response to a query of our external ip and port seen by the lobby server. EventData is <see cref="SeenExternalIpAndPort"/> with the clients seen external address.
/// </summary>
ExternalIpAndPort
}
}

View File

@ -1,52 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using static System.Runtime.InteropServices.JavaScript.JSType;
namespace LobbyClientTest
{
internal class FakeGameHost
internal class FakeGameHost : IDisposable
{
private Socket _socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
private const int bufSize = 8 * 1024;
private State state = new State();
private EndPoint epFrom = new IPEndPoint(IPAddress.Any, 0);
private AsyncCallback recv = null;
public bool isHost;
public class State
{
public byte[] buffer = new byte[bufSize];
}
public bool isHost = false;
UdpClient? udpClient;
bool running = true;
public int Server(int port)
{
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true);
_socket.ExclusiveAddressUse = false;
_socket.Bind(new IPEndPoint(IPAddress.Any, port));
udpClient = new UdpClient(port);
_ = Task.Run(() =>
{
Receive();
return ((IPEndPoint)_socket.LocalEndPoint!).Port;
});
return ((IPEndPoint)udpClient.Client.LocalEndPoint!).Port;
}
public void Send(EndPoint ep, string text)
public void Send(IPEndPoint ep, string text)
{
byte[] data = Encoding.ASCII.GetBytes(text);
_socket.SendTo(data, 0, data.Length, SocketFlags.None, ep);
udpClient?.Send(data, data.Length, ep);
}
public void Send(IPEndPoint ep, byte[] message, int length)
{
udpClient?.Send(message, length, ep);
}
private void Receive()
{
_socket.BeginReceiveFrom(state.buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv = (ar) =>
try
{
State so = (State)ar.AsyncState;
int bytes = _socket.EndReceiveFrom(ar, ref epFrom);
_socket.BeginReceiveFrom(so.buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv, so);
Console.WriteLine($"Game {(isHost ? "host" : "client")} received: {epFrom.ToString()}: {bytes}, {Encoding.ASCII.GetString(so.buffer, 0, bytes)}");
IPEndPoint remoteEp = new IPEndPoint(IPAddress.Any, 0);
while (running)
{
var data = udpClient!.Receive(ref remoteEp);
Console.WriteLine($"Game {(isHost ? "host" : "client")} received: {remoteEp.ToString()}: {data.Length}, {Encoding.ASCII.GetString(data, 0, data.Length)}");
if (isHost)
Send(epFrom, "Hello from Game Server!");
}, state);
Send(remoteEp, "Hello from Game Server!");
}
}
catch
{
}
finally
{
}
}
public void Dispose()
{
running = false;
udpClient?.Dispose();
}
}
}

View File

@ -4,19 +4,22 @@ using LobbyClientTest;
using LobbyServerDto;
using System.Net;
Console.WriteLine("Starting lobby client!");
Console.WriteLine("Starting lobby client v0.7!");
var lobbyClient = new LobbyClient();
var cancellationTokenSource = new CancellationTokenSource();
List<LobbyInfo> openLobbies = new List<LobbyInfo>();
lobbyClient.Connect("localhost", 8088, cancellationTokenSource.Token);
lobbyClient.Connect("lobby.incobyte.de" /*"localhost"*/, 8088, cancellationTokenSource.Token);
FakeGameHost fakeGameHost = new FakeGameHost();
int myPort = fakeGameHost.Server(0);
string? myExternalIp = null;
int myExternalPort = -1;
IPEndPoint? hostInfo = null;
bool running = true;
bool connected = false;
_ = Task.Run(() =>
{
@ -28,6 +31,7 @@ _ = Task.Run(() =>
{
case LobbyClientEventTypes.Connected:
{
connected = true;
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine("Lobby client connected!");
@ -76,24 +80,56 @@ _ = Task.Run(() =>
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Host info for lobby {lobbyHostInfo!.LobbyId} is {lobbyHostInfo.HostIp}:{lobbyHostInfo.HostPort}!");
hostInfo = new IPEndPoint(IPAddress.Parse(lobbyHostInfo.HostIp!), lobbyHostInfo.HostPort);
Console.WriteLine($"Requesting nat punch!");
lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, null, myPort);
Console.WriteLine($"Requesting nat punch to me!");
lobbyClient.RequestLobbyNatPunch(lobbyHostInfo.LobbyId, null, (remoteEndpoint, messageBuffer, messageLength) => {
fakeGameHost.Send(remoteEndpoint, messageBuffer, messageLength);
});
Console.Write(">");
}
break;
case LobbyClientEventTypes.ExternalIpAndPort:
{
var seenExternalIpAndPort = lobbyEvent.EventData as SeenExternalIpAndPort;
if (seenExternalIpAndPort != null)
{
myExternalIp = seenExternalIpAndPort.Ip;
myExternalPort = seenExternalIpAndPort.Port;
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Received my external ip {seenExternalIpAndPort!.Ip}:{seenExternalIpAndPort.Port}");
Console.Write(">");
connected = true;
}
}
break;
case LobbyClientEventTypes.LobbyRequestNatPunch:
{
var lobbyRequestNatPunch = lobbyEvent.EventData as LobbyRequestNatPunch;
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch requested to {lobbyRequestNatPunch!.ClientIp}:{lobbyRequestNatPunch.ClientPort}!");
//NatPuncher.NatPunch(new IPEndPoint(IPAddress.Any, myPort), new IPEndPoint(IPAddress.Parse(lobbyRequestNatPunch.ClientIp!), lobbyRequestNatPunch.ClientPort));
Task.Run(() =>
{
lobbyClient.QueryExternalIpAndPort((remoteEndpoint, messageData, messageLength) => {
fakeGameHost.Send(remoteEndpoint, messageData, messageLength);
});
var ep = new IPEndPoint(IPAddress.Parse(lobbyRequestNatPunch.ClientIp!), lobbyRequestNatPunch.ClientPort);
for (int z = 0; z < 16; z++)
for (int z = 0; z < 32; z++)
{
fakeGameHost.Send(ep, "Nat Falcon Punch!");
}
lobbyClient.NotifyLobbyNatPunchDone(lobbyRequestNatPunch.NatPunchId);
lobbyClient.NotifyLobbyNatPunchDone(lobbyRequestNatPunch.NatPunchId, lobbyClient.externalIp!, lobbyClient.externalPort);
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch done!");
Console.Write(">");
});
Console.Write(">");
}
break;
@ -104,7 +140,8 @@ _ = Task.Run(() =>
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch request done!");
Console.WriteLine($"Connecting game client!");
fakeGameHost.Send(hostInfo!, "Hello from Game Client!");
fakeGameHost.Send(new IPEndPoint(IPAddress.Parse(lobbyNatPunchDone!.ExternalIp!), lobbyNatPunchDone.ExternalPort), "Hello from Game Client!");
Console.Write(">");
}
break;
@ -132,11 +169,18 @@ while (running)
switch (line)
{
case "host":
{
if (!connected)
{
Console.WriteLine("Not connected yet!");
}
else
{
Console.WriteLine("Hosting game ...");
lobbyClient.HostLobby(GameGuids.NFS, "Hallo, Welt!", 1, 8, null, null, myPort);
lobbyClient.HostLobby(GameGuids.NFS, "Hallo, Welt!", 1, 8, null, "127.0.0.1", myPort);
fakeGameHost.isHost = true;
}
}
break;
case "host stop":
{
@ -146,6 +190,12 @@ while (running)
}
break;
case "join":
{
if (!connected)
{
Console.WriteLine("Not connected yet!");
}
else
{
Console.WriteLine("Trying to join first lobby ...");
var firstLobby = openLobbies.FirstOrDefault();
@ -158,6 +208,7 @@ while (running)
Console.WriteLine("Seeing no open lobby!");
}
}
}
break;
case "observe":
{

View File

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using var closing = new AutoResetEvent(false);
using var tcpServer = new TcpServer();
using var udpServer = new UdpCandidateServer();
ConcurrentDictionary<Guid, Lobby> lobbiesById = new ConcurrentDictionary<Guid, Lobby>();
ConcurrentDictionary<Guid, List<Lobby>> lobbiesByGameId = new ConcurrentDictionary<Guid, List<Lobby>>();
@ -14,35 +15,23 @@ ConcurrentDictionary<int, Guid> clientWatchingGameIdLobbies = new ConcurrentDict
ConcurrentDictionary<Guid, List<int>> clientsWatchingGameId = new ConcurrentDictionary<Guid, List<int>>();
BufferRental bufferRental = new BufferRental(4096);
int PeekTypeId(ReadOnlySpan<byte> buffer)
udpServer.QueryIpAndPort += (clientId, ip, port) =>
{
int typeId = 0;
int shift = 0;
int offset = 0;
byte b;
do
var messageData = bufferRental.Rent();
var seenExternalIpAndPort = new SeenExternalIpAndPort() { Ip = ip, Port = port };
var messageDataLength = seenExternalIpAndPort.Serialize(messageData);
_ = Task.Run(async () =>
{
{
// Check for a corrupted stream. Read a max of 5 bytes.
// In a future version, add a DataFormatException.
if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7
throw new FormatException("Format_Bad7BitInt32");
// ReadByte handles end of stream cases for us.
b = buffer[offset++];
typeId |= (b & 0x7F) << shift;
shift += 7;
}
} while ((b & 0x80) != 0);
return typeId;
}
await tcpServer.Send(clientId, messageData, 0, messageDataLength);
bufferRental.Return(messageData);
});
};
tcpServer.DataReceived += (clientId, dataLength, data) =>
{
if (dataLength > 0)
{
switch (PeekTypeId(data.Span))
switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(data.Span))
{
case LobbyCreate.TypeId:
{
@ -433,8 +422,9 @@ Console.CancelKeyPress += (sender, args) =>
args.Cancel = true;
};
Console.WriteLine($"{DateTime.Now}: Application started");
Console.WriteLine($"{DateTime.Now}: Application started v0.7");
udpServer.Start(8088);
tcpServer.Start(8088);
closing.WaitOne();

View File

@ -1,7 +1,7 @@
using System.Net.Sockets;
using System.Net;
using System.Collections.Concurrent;
using LobbyServerDto;
namespace LobbyServer
{
@ -131,6 +131,11 @@ namespace LobbyServer
bool offsetSizeInt = false;
int currentReadOffset = 0;
var lobbyClientConnectionInfo = new LobbyClientConnectionInfo { Id = myId };
byte[] sendBuffer = new byte[128];
int sendLen = lobbyClientConnectionInfo.Serialize(sendBuffer);
await Send(myId, sendBuffer, 0, sendLen);
while (running)
{
int copyOffset = 0;

View File

@ -0,0 +1,93 @@
using System.Net.Sockets;
using System.Net;
using LobbyServerDto;
namespace LobbyServer
{
/// <summary>
/// Small udp server to receive udp packets and report their remote ip and port to a event delegate
/// </summary>
internal class UdpCandidateServer : IDisposable
{
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
private bool running = false;
private bool isDisposed = false;
public delegate void QueryIpAndPortReceivedEventArgs(int clientId, string ip, int port);
/// <summary>
/// If a valid request for a ip and port query comes in call this event with the seen remote ip and port for a lobby server client id
/// </summary>
public event QueryIpAndPortReceivedEventArgs? QueryIpAndPort;
/// <summary>
/// Listen to requests and fire events
/// </summary>
/// <param name="port">The port to listen on</param>
private async void Listen(int port)
{
try
{
cancellationTokenSource.Token.ThrowIfCancellationRequested();
using var serverSocket = new UdpClient(port, AddressFamily.InterNetwork);
Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] started");
while (running)
{
var receiveResult = await serverSocket.ReceiveAsync(cancellationTokenSource.Token);
if(receiveResult.Buffer.Length > 0)
{
Memory<byte> receivedData = new Memory<byte>(receiveResult.Buffer);
switch (LobbyMessageIdentifier.ReadLobbyMessageIdentifier(receivedData.Span))
{
case QueryExternalPortAndIp.TypeId:
var queryExternalPortAndIp = QueryExternalPortAndIp.Deserialize(receivedData.Span);
QueryIpAndPort?.Invoke(queryExternalPortAndIp.LobbyClientId, receiveResult.RemoteEndPoint.Address.ToString(), receiveResult.RemoteEndPoint.Port);
break;
}
}
}
}
catch when (cancellationTokenSource.IsCancellationRequested) //Cancel requested
{
Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] canceled");
}
catch(Exception e)
{
Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] exception: {e}");
throw;
}
finally
{
Console.WriteLine($"{DateTime.Now}: [UdpCandidateServer] stopped");
}
}
/// <summary>
/// Start udp listener
/// </summary>
/// <param name="port">The port to listen on</param>
public void Start(int port)
{
running = true;
_ = Task.Run(() => Listen(port));
}
/// <summary>
/// Stop udp listener
/// </summary>
public void Stop()
{
running = false;
cancellationTokenSource.Cancel();
}
public void Dispose()
{
if (!isDisposed)
{
cancellationTokenSource.Dispose();
isDisposed = true;
}
}
}
}

View File

@ -1,8 +1,15 @@
namespace LobbyServerDto
{
/// <summary>
/// Used to subscribe to a game id by guid to observe it's list of open lobbies
/// </summary>
/// <remarks>A client can only be subscribed to one game id, sending multiple observe requests with different game ids will overwrite the previous</remarks>
[LobbyMessage]
public partial class LobbiesObserve
{
/// <summary>
/// The game id to subscribe to
/// </summary>
public Guid GameId { get; set; }
}
}

View File

@ -1,5 +1,8 @@
namespace LobbyServerDto
{
/// <summary>
/// Stop observing and receiving updates on the lobby list
/// </summary>
[LobbyMessage]
public partial class LobbiesStopObserve
{

View File

@ -0,0 +1,14 @@
namespace LobbyServerDto
{
/// <summary>
/// Send to lobby client after connection is established
/// </summary>
[LobbyMessage]
public partial class LobbyClientConnectionInfo
{
/// <summary>
/// Connection id on the lobby server
/// </summary>
public int Id { get; set; }
}
}

View File

@ -2,19 +2,47 @@
namespace LobbyServerDto
{
/// <summary>
/// Create a lobby in the lobby list. The lobby will stay until either closed by the host or the host disconnects
/// </summary>
/// <remarks>A host can always have only one open lobby, if another lobby is already opened by the client it will be closed</remarks>
[LobbyMessage]
public partial class LobbyCreate
{
/// <summary>
/// Game Id for which game this lobby is for
/// </summary>
public Guid GameId { get; set; }
/// <summary>
/// Display name of the lobby
/// </summary>
[MaxLength(64)]
public string Name { get; set; } = string.Empty;
/// <summary>
/// Game mode of lobby
/// </summary>
public int GameMode { get; set; }
/// <summary>
/// How many players are in the lobby
/// </summary>
public int PlayerCount { get; set; }
/// <summary>
/// Maximum players allowed in the lobby
/// </summary>
public int MaxPlayerCount { get; set; }
/// <summary>
/// The hash of a password to protect the lobby. Only users with the password can request host information/nat punch.
/// </summary>
[MaxLength(26)]
public byte[]? PasswordHash { get; set; }
/// <summary>
/// The hosts ip. Used the the host information send to clients on their request.
/// </summary>
[MaxLength(32)]
public string? HostIp { get; set; }
/// <summary>
/// The hosts port. Used the the host information send to clients on their request.
/// </summary>
public int HostPort { get; set; }
}
}

View File

@ -1,9 +1,19 @@
namespace LobbyServerDto
{
/// <summary>
/// If send from host closes and deletes a lobby.
/// If received by client, the lobby with the Id parameter has been closed
/// </summary>
[LobbyMessage]
public partial class LobbyDelete
{
/// <summary>
/// If received by client, this contains the lobby id. Does not need to be set if the host sends this request.
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// If received by client, this contains the game id. Does not need to be set if the host sends this request.
/// </summary>
public Guid GameId { get; set; }
}
}

View File

@ -2,12 +2,24 @@
namespace LobbyServerDto
{
/// <summary>
/// Send to client upon request, this contains the connection info supplied by the host.
/// </summary>
[LobbyMessage]
public partial class LobbyHostInfo
{
/// <summary>
/// Lobby id this information is for
/// </summary>
public Guid LobbyId { get; set; }
/// <summary>
/// The hosts ip, this could be an internal address
/// </summary>
[MaxLength(32)]
public string? HostIp { get; set; }
/// <summary>
/// The hosts port
/// </summary>
public int HostPort { get; set; }
}
}

View File

@ -1,13 +1,34 @@
namespace LobbyServerDto
{
/// <summary>
/// Send when a lobby is created or updates, this contains the lobby information
/// </summary>
[LobbyMessage]
public partial class LobbyInfo
{
/// <summary>
/// Id of the lobby
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// Displayname of the lobby
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Game mode of the lobby
/// </summary>
public int GameMode { get; set; }
/// <summary>
/// Count of currently joined players in the lobby
/// </summary>
public int PlayerCount { get; set; }
/// <summary>
/// Max players allowed in the lobby
/// </summary>
public int MaxPlayerCount { get; set; }
/// <summary>
/// True if the lobby requires a password to gain access
/// </summary>
public bool PasswordProtected { get; set; }
}
}

View File

@ -0,0 +1,44 @@
namespace LobbyServerDto
{
/// <summary>
/// Used to read the message identifer from a received object
/// </summary>
public static class LobbyMessageIdentifier
{
/// <summary>
/// Read the message identifiere from the stream
/// </summary>
/// <param name="buffer"></param>
/// <returns>The message identifier or -1 if invalid</returns>
public static int ReadLobbyMessageIdentifier(ReadOnlySpan<byte> buffer)
{
if(buffer.Length == 0)
return -1;
int typeId = 0;
int shift = 0;
int offset = 0;
byte currentByte;
do
{
if (offset < buffer.Length)
{
// Check for a corrupted stream. Read a max of 5 bytes.
// In a future version, add a DataFormatException.
if (shift == 5 * 7) // 5 bytes max per Int32, shift += 7
return -1;
// ReadByte handles end of stream cases for us.
currentByte = buffer[offset++];
typeId |= (currentByte & 0x7F) << shift;
shift += 7;
}
else
return -1;
} while ((currentByte & 0x80) != 0);
return typeId;
}
}
}

View File

@ -2,10 +2,29 @@
namespace LobbyServerDto
{
/// <summary>
/// Send from host to lobby server and releyed to the client by lobby server if the nat punch is finished.
/// </summary>
[LobbyMessage]
public partial class LobbyNatPunchDone
{
/// <summary>
/// The lobby this nat punch was for
/// </summary>
public Guid LobbyId { get; set; }
/// <summary>
/// Id of the nat punch that the lobby server requested from us in <see cref="LobbyRequestNatPunch"/>
/// </summary>
public int NatPunchId { get; set; }
/// <summary>
/// The external ip of the host
/// </summary>
[MaxLength(32)]
public string? ExternalIp { get; set; }
/// <summary>
/// The external port of the host
/// </summary>
public int ExternalPort { get; set; }
}
}

View File

@ -2,10 +2,19 @@
namespace LobbyServerDto
{
/// <summary>
/// Request the host address for a lobby
/// </summary>
[LobbyMessage]
public partial class LobbyRequestHostInfo
{
/// <summary>
/// Id of the lobby
/// </summary>
public Guid LobbyId { get; set; }
/// <summary>
/// Hash of the password if the lobby requires one
/// </summary>
[MaxLength(26)]
public byte[]? PasswordHash { get; set; }
}

View File

@ -2,16 +2,33 @@
namespace LobbyServerDto
{
/// <summary>
/// Request a nat punch from the game host
/// </summary>
[LobbyMessage]
public partial class LobbyRequestNatPunch
{
/// <summary>
/// Id of the lobby
/// </summary>
public Guid LobbyId { get; set; }
/// <summary>
/// Password hash if the lobby is password protected
/// </summary>
[MaxLength(26)]
public byte[]? PasswordHash { get; set; }
/// <summary>
/// Our external ip
/// </summary>
public string? ClientIp { get; set; }
/// <summary>
/// Our external port
/// </summary>
public int ClientPort { get; set; }
/// <summary>
/// Set by the lobby server, this will be the NatPunchId send to the host
/// </summary>
public int NatPunchId { get; set; }
}
}

View File

@ -2,16 +2,40 @@
namespace LobbyServerDto
{
/// <summary>
/// If the host updates values of the lobby, it can send this request to update the lobby information on the lobby server
/// </summary>
[LobbyMessage]
public partial class LobbyUpdate
{
/// <summary>
/// The displayname of the lobby
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// The game mode of the lobby
/// </summary>
public int GameMode { get; set; }
/// <summary>
/// Current count of players in the lobby
/// </summary>
public int PlayerCount { get; set; }
/// <summary>
/// Max players in the lobby
/// </summary>
public int MaxPlayerCount { get; set; }
/// <summary>
/// The hash of the password required to enter this lobby
/// </summary>
public byte[]? PasswordHash { get; set; }
/// <summary>
/// The hosts ip
/// </summary>
[MaxLength(32)]
public string? HostIp { get; set; }
/// <summary>
/// The hosts port
/// </summary>
public int HostPort { get; set; }
}
}

View File

@ -1,8 +1,14 @@
namespace LobbyServerDto
{
/// <summary>
/// Send if a client requests information about a password protected lobby with an invalid password hash
/// </summary>
[LobbyMessage]
public partial class LobbyWrongPassword
{
/// <summary>
/// Id of the lobby
/// </summary>
public Guid LobbyId { get; set; }
}
}

View File

@ -1,29 +0,0 @@
using System.Net;
using System.Net.Sockets;
namespace LobbyServerDto
{
public class NatPuncher
{
public static void NatPunch(IPEndPoint localEndpoint, IPEndPoint remoteEndpoint)
{
try
{
using Socket socket = new Socket(SocketType.Dgram, ProtocolType.Udp);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true);
socket.ExclusiveAddressUse = false;
socket.Bind(localEndpoint);
for (int i = 0; i < 16; i++)
{
socket.SendTo(new byte[] { }, remoteEndpoint);
}
}
catch
{
throw;
}
}
}
}

View File

@ -0,0 +1,15 @@

namespace LobbyServerDto
{
/// <summary>
/// Send from a client to the lobby server via udp to from the game port to retrieve the observerd external ip and port
/// </summary>
[LobbyMessage]
public partial class QueryExternalPortAndIp
{
/// <summary>
/// Lobby client id
/// </summary>
public int LobbyClientId { get; set; }
}
}

View File

@ -0,0 +1,22 @@

using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
/// <summary>
/// The ip and port seen by the lobby udp server by an <see cref="QueryExternalPortAndIp"/> request.
/// </summary>
[LobbyMessage]
public partial class SeenExternalIpAndPort
{
/// <summary>
/// The seen ip of the client
/// </summary>
[MaxLength(32)]
public string? Ip { get; set; }
/// <summary>
/// The seen port of the client
/// </summary>
public int Port { get; set; }
}
}

View File

@ -1,6 +1,9 @@

namespace LobbyServerDto
{
/// <summary>
/// List of accepted game ips
/// </summary>
public static class GameGuids
{
public static Guid NFS = new Guid("0572706f-0fd9-46a9-8ea6-5a31a5363442");