Initial
parent
2674eb2a10
commit
840218f119
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
namespace LobbyServerDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Stop observing and receiving updates on the lobby list
|
||||
/// </summary>
|
||||
[LobbyMessage]
|
||||
public partial class LobbiesStopObserve
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in New Issue