Projektdateien hinzufügen.

main
Thomas Woischnig 2023-11-28 00:57:50 +01:00
parent cbbf47885d
commit 2674eb2a10
32 changed files with 2091 additions and 0 deletions

275
LobbyClient/LobbyClient.cs Normal file
View File

@ -0,0 +1,275 @@
using LobbyServerDto;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Lobbies
{
public class LobbyClient : IDisposable
{
TcpLobbbyClient tcpClient = new TcpLobbbyClient();
private readonly ConcurrentQueue<LobbyClientEvent> events = new ConcurrentQueue<LobbyClientEvent>();
BufferRental bufferRental = new BufferRental(4096);
public void Connect(string host, int port, CancellationToken cancellationToken)
{
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;
tcpClient.Connected -= TcpClient_Connected;
tcpClient.Connected += TcpClient_Connected;
_ = Task.Run(() => tcpClient.Connect(host, port));
}
catch
{
}
}
public IEnumerable<LobbyClientEvent> 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)
{
var lobbyCreate = new LobbyCreate()
{
GameId = gameId,
Name = name,
GameMode = gameMode,
MaxPlayerCount = maxPlayerCount,
PlayerCount = 0,
PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)),
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)
{
var lobbyCreate = new LobbyRequestHostInfo()
{
LobbyId = lobbyId,
PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)),
};
byte[] messageData = bufferRental.Rent();
var len = lobbyCreate.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void RequestLobbyNatPunch(Guid lobbyId, string? password, string? clientIp, int clientPort)
{
var lobbyRequestNatPunch = new LobbyRequestNatPunch()
{
LobbyId = lobbyId,
PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)),
ClientIp = clientIp,
ClientPort = clientPort
};
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)
{
var lobbyUpdate = new LobbyUpdate()
{
Name = name,
GameMode = gameMode,
MaxPlayerCount = maxPlayerCount,
PlayerCount = playerCount,
PasswordHash = string.IsNullOrEmpty(password) ? null : SHA256.HashData(Encoding.UTF8.GetBytes(password)),
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)
{
var lobbyNatPunchDone = new LobbyNatPunchDone() { NatPunchId = natPunchId };
byte[] messageData = bufferRental.Rent();
var len = lobbyNatPunchDone.Serialize(messageData);
_ = Task.Run(async () => { await tcpClient.Send(messageData, 0, len); bufferRental.Return(messageData); });
}
public void Stop()
{
tcpClient.Stop();
}
private void TcpClient_Connected()
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.Connected, EventData = null });
}
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 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))
{
case LobbyInfo.TypeId:
{
var lobbyInfo = LobbyInfo.Deserialize(data.Span);
if (lobbyInfo != null)
{
events.Enqueue(new LobbyClientEvent { EventType = LobbyClientEventTypes.LobbyUpdate, EventData = lobbyInfo });
}
}
break;
case LobbyDelete.TypeId:
{
var lobbyDelete = LobbyDelete.Deserialize(data.Span);
if (lobbyDelete != null)
{
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;
}
}
}
catch { }
}
public void Dispose()
{
tcpClient.Dispose();
}
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LobbyServerDto\LobbyServerDto.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lobbies
{
public class LobbyClientDisconnectReason
{
public bool WasError { get; internal set; }
public string? ErrorMessage { get; internal set; }
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Lobbies
{
public class LobbyClientEvent
{
public LobbyClientEventTypes EventType { get; internal set; }
public object? EventData { get; internal set; }
}
public enum LobbyClientEventTypes
{
Connected,
Disconnected,
Failed,
LobbyAdd,
LobbyUpdate,
LobbyDelete,
LobbyHostInfo,
LobbyRequestNatPunch,
LobbyNatPunchDone,
}
}

161
LobbyClient/TcpClient.cs Normal file
View File

@ -0,0 +1,161 @@
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Lobbies
{
internal class TcpLobbbyClient : IDisposable
{
internal delegate void DataReceivedEventArgs(int bytes, Memory<byte> data);
internal event DataReceivedEventArgs? DataReceived;
internal delegate void DisconnectedEventArgs(bool clean, string error);
internal event DisconnectedEventArgs? Disconnected;
internal delegate void ConnectedEventArgs();
internal event ConnectedEventArgs? Connected;
TcpClient? tcpClient;
NetworkStream? networkStream;
CancellationTokenSource? cancellationTokenSource = new CancellationTokenSource();
bool running = false;
internal async Task Connect(string host, int port)
{
bool wasError = false;
string error = string.Empty;
try
{
running = true;
tcpClient = new TcpClient();
await tcpClient.ConnectAsync(host, port, cancellationTokenSource!.Token);
networkStream = tcpClient.GetStream();
Memory<byte> buffer = new byte[4096];
Memory<byte> target = new byte[4096];
int currentOffset = 0;
int currentMessageRemainingLength = 0;
int currentMessageLength = 0;
bool validMessage = true;
int currentReadOffset = 0;
bool offsetSizeInt = false;
Connected?.Invoke();
while (running)
{
int copyOffset = 0;
int receivedBytes = currentReadOffset;
if (currentReadOffset < 4)
{
receivedBytes += await networkStream.ReadAsync(buffer.Slice(currentReadOffset), cancellationTokenSource.Token) + currentReadOffset;
}
if (receivedBytes == 0 && running && !cancellationTokenSource.Token.IsCancellationRequested)
{
throw new Exception("Connection lost!");
}
if (receivedBytes > 3 || (currentMessageRemainingLength > 0 && receivedBytes > currentMessageRemainingLength))
{
currentReadOffset = 0;
if (currentMessageLength == 0)
{
currentMessageRemainingLength = BitConverter.ToInt32(buffer.Span);
currentMessageLength = currentMessageRemainingLength;
receivedBytes -= 4;
copyOffset += 4;
offsetSizeInt = true;
}
else
offsetSizeInt = false;
var receivedCount = Math.Min(receivedBytes, currentMessageRemainingLength);
receivedBytes -= receivedCount;
copyOffset += receivedCount;
if (validMessage && currentOffset + receivedCount > 0)
{
if (currentOffset + receivedCount < target.Length)
buffer.Slice(offsetSizeInt ? 4 : 0, receivedCount).CopyTo(target.Slice(currentOffset));
else
validMessage = false;
}
currentOffset += receivedCount;
currentMessageRemainingLength -= receivedCount;
if (currentMessageRemainingLength <= 0)
{
if (validMessage)
DataReceived?.Invoke(currentMessageLength, target);
if (receivedBytes > 0)
{
buffer.Slice(copyOffset, receivedBytes).CopyTo(buffer);
currentReadOffset += receivedBytes;
}
currentOffset = 0;
currentMessageLength = 0;
currentMessageRemainingLength = 0;
validMessage = true;
}
}
else if (receivedBytes > 0)
{
currentReadOffset += receivedBytes;
}
}
}
catch(Exception e)
{
error = e.Message;
}
finally
{
wasError = running;
running = false;
networkStream?.Dispose();
tcpClient?.Dispose();
tcpClient = null;
networkStream = null;
Disconnected?.Invoke(!wasError, error);
}
}
internal async Task Send(byte[] buffer, int offset, int count)
{
try
{
if (running && networkStream != null)
{
await networkStream.WriteAsync(BitConverter.GetBytes(count - offset), 0, 4, cancellationTokenSource!.Token);
await networkStream.WriteAsync(buffer, offset, count, cancellationTokenSource!.Token);
}
}
catch { }
}
internal void Stop()
{
running = false;
cancellationTokenSource?.Cancel();
}
public void Dispose()
{
cancellationTokenSource?.Cancel();
tcpClient?.Dispose();
cancellationTokenSource?.Dispose();
}
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace LobbyClientTest
{
internal class FakeGameHost
{
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 int Server(int port)
{
_socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.ReuseAddress, true);
_socket.ExclusiveAddressUse = false;
_socket.Bind(new IPEndPoint(IPAddress.Any, port));
Receive();
return ((IPEndPoint)_socket.LocalEndPoint!).Port;
}
public void Send(EndPoint ep, string text)
{
byte[] data = Encoding.ASCII.GetBytes(text);
_socket.SendTo(data, 0, data.Length, SocketFlags.None, ep);
}
private void Receive()
{
_socket.BeginReceiveFrom(state.buffer, 0, bufSize, SocketFlags.None, ref epFrom, recv = (ar) =>
{
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)}");
if (isHost)
Send(epFrom, "Hello from Game Server!");
}, state);
}
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LobbyClient\LobbyClient.csproj" />
</ItemGroup>
</Project>

184
LobbyClientTest/Program.cs Normal file
View File

@ -0,0 +1,184 @@
// See https://aka.ms/new-console-template for more information
using Lobbies;
using LobbyClientTest;
using LobbyServerDto;
using System.Net;
Console.WriteLine("Starting lobby client!");
var lobbyClient = new LobbyClient();
var cancellationTokenSource = new CancellationTokenSource();
List<LobbyInfo> openLobbies = new List<LobbyInfo>();
lobbyClient.Connect("localhost", 8088, cancellationTokenSource.Token);
FakeGameHost fakeGameHost = new FakeGameHost();
int myPort = fakeGameHost.Server(0);
IPEndPoint? hostInfo = null;
bool running = true;
_ = Task.Run(() =>
{
while (running)
{
foreach (var lobbyEvent in lobbyClient.ReadEvents(20))
{
switch (lobbyEvent.EventType)
{
case LobbyClientEventTypes.Connected:
{
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine("Lobby client connected!");
Console.Write(">");
lobbyClient.ObserveLobbies(GameGuids.NFS);
}
break;
case LobbyClientEventTypes.LobbyAdd:
case LobbyClientEventTypes.LobbyUpdate:
{
var lobbyInfo = lobbyEvent.EventData as LobbyInfo;
openLobbies.Add(lobbyInfo!);
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"LobbyInfo: {lobbyInfo!.Id}, name: {lobbyInfo.Name}, mode: {lobbyInfo.GameMode}, maxplayercount: {lobbyInfo.MaxPlayerCount}, playercount: {lobbyInfo.PlayerCount}, password: {lobbyInfo.PasswordProtected}");
Console.Write(">");
}
break;
case LobbyClientEventTypes.LobbyDelete:
{
var lobbyDelete = lobbyEvent.EventData as LobbyDelete;
var existingLobby = openLobbies.FirstOrDefault(l => l.Id == lobbyDelete!.Id);
if (existingLobby != null)
openLobbies.Remove(existingLobby);
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"LobbyDelete: {lobbyDelete!.Id}");
Console.Write(">");
}
break;
case LobbyClientEventTypes.Failed:
{
var reason = lobbyEvent.EventData as LobbyClientDisconnectReason;
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Lobby connection failed! WasError: {reason!.WasError}, error: {reason.ErrorMessage}");
running = false;
}
break;
case LobbyClientEventTypes.LobbyHostInfo:
{
var lobbyHostInfo = lobbyEvent.EventData as LobbyHostInfo;
var p = Console.GetCursorPosition();
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.Write(">");
}
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));
var ep = new IPEndPoint(IPAddress.Parse(lobbyRequestNatPunch.ClientIp!), lobbyRequestNatPunch.ClientPort);
for (int z = 0; z < 16; z++)
{
fakeGameHost.Send(ep, "Nat Falcon Punch!");
}
lobbyClient.NotifyLobbyNatPunchDone(lobbyRequestNatPunch.NatPunchId);
Console.Write(">");
}
break;
case LobbyClientEventTypes.LobbyNatPunchDone:
{
var lobbyNatPunchDone = lobbyEvent.EventData as LobbyNatPunchDone;
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Nat punch request done!");
Console.WriteLine($"Connecting game client!");
fakeGameHost.Send(hostInfo!, "Hello from Game Client!");
Console.Write(">");
}
break;
case LobbyClientEventTypes.Disconnected:
{
var p = Console.GetCursorPosition();
Console.SetCursorPosition(0, p.Top);
Console.WriteLine($"Lobby disonnected!");
running = false;
}
break;
}
}
Thread.Sleep(10);
}
});
while (running)
{
Console.Write(">");
var line = Console.ReadLine();
if (line != null)
{
switch (line)
{
case "host":
{
Console.WriteLine("Hosting game ...");
lobbyClient.HostLobby(GameGuids.NFS, "Hallo, Welt!", 1, 8, null, null, myPort);
fakeGameHost.isHost = true;
}
break;
case "host stop":
{
Console.WriteLine("Stop hosting game ...");
lobbyClient.CloseLobby();
fakeGameHost.isHost = false;
}
break;
case "join":
{
Console.WriteLine("Trying to join first lobby ...");
var firstLobby = openLobbies.FirstOrDefault();
if (firstLobby != null)
{
lobbyClient.RequestLobbyHostInfo(firstLobby.Id, null);
}
else
{
Console.WriteLine("Seeing no open lobby!");
}
}
break;
case "observe":
{
Console.WriteLine("Observing lobby list ...");
lobbyClient.ObserveLobbies(GameGuids.NFS);
}
break;
case "observe stop":
{
Console.WriteLine("Stop observing lobby list ...");
lobbyClient.StopObservingLobbies();
}
break;
case "exit":
running = false;
break;
}
}
}
lobbyClient.Stop();
lobbyClient.Dispose();

49
LobbyServer.sln Normal file
View File

@ -0,0 +1,49 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.7.34003.232
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LobbyServer", "LobbyServer\LobbyServer.csproj", "{64B89314-4185-4025-B8B9-AC0D3A921E6A}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LobbyServerDto", "LobbyServerDto\LobbyServerDto.csproj", "{5AA6CC31-3A59-4463-8E25-56852430765C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LobbyServerSourceGenerator", "LobbyServerSourceGenerator\LobbyServerSourceGenerator.csproj", "{5353E418-2365-432B-ACC6-C20448F93CC9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LobbyClient", "LobbyClient\LobbyClient.csproj", "{1D6DE49F-7A41-4117-A9AF-6EE3417948EB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LobbyClientTest", "LobbyClientTest\LobbyClientTest.csproj", "{2A5901FE-CE35-4C81-9B8A-E8180EAE7465}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{64B89314-4185-4025-B8B9-AC0D3A921E6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64B89314-4185-4025-B8B9-AC0D3A921E6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64B89314-4185-4025-B8B9-AC0D3A921E6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64B89314-4185-4025-B8B9-AC0D3A921E6A}.Release|Any CPU.Build.0 = Release|Any CPU
{5AA6CC31-3A59-4463-8E25-56852430765C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5AA6CC31-3A59-4463-8E25-56852430765C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5AA6CC31-3A59-4463-8E25-56852430765C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5AA6CC31-3A59-4463-8E25-56852430765C}.Release|Any CPU.Build.0 = Release|Any CPU
{5353E418-2365-432B-ACC6-C20448F93CC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5353E418-2365-432B-ACC6-C20448F93CC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5353E418-2365-432B-ACC6-C20448F93CC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5353E418-2365-432B-ACC6-C20448F93CC9}.Release|Any CPU.Build.0 = Release|Any CPU
{1D6DE49F-7A41-4117-A9AF-6EE3417948EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1D6DE49F-7A41-4117-A9AF-6EE3417948EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1D6DE49F-7A41-4117-A9AF-6EE3417948EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1D6DE49F-7A41-4117-A9AF-6EE3417948EB}.Release|Any CPU.Build.0 = Release|Any CPU
{2A5901FE-CE35-4C81-9B8A-E8180EAE7465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A5901FE-CE35-4C81-9B8A-E8180EAE7465}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A5901FE-CE35-4C81-9B8A-E8180EAE7465}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A5901FE-CE35-4C81-9B8A-E8180EAE7465}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6FBBD037-DB88-41C4-A377-9F93163E5AA8}
EndGlobalSection
EndGlobal

29
LobbyServer/Lobby.cs Normal file
View File

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LobbyServer
{
internal class Lobby
{
internal enum LobbyUpdateType
{
Add,
Update,
Delete
};
public Guid Id { get; set; } = Guid.NewGuid();
public Guid GameId { get; set; }
public int HostClientId { get; set; }
public required string Name { get; set; }
public int GameMode { get; set; }
public int PlayerCount { get; set; }
public int MaxPlayerCount { get; set; }
public byte[]? PasswordHash { get; set; }
public required string HostIp { get; set; }
public int HostPort { get; set; }
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LobbyServerDto\LobbyServerDto.csproj" />
</ItemGroup>
</Project>

442
LobbyServer/Program.cs Normal file
View File

@ -0,0 +1,442 @@
using LobbyServer;
using LobbyServerDto;
using System.Collections.Concurrent;
using var closing = new AutoResetEvent(false);
using var tcpServer = new TcpServer();
ConcurrentDictionary<Guid, Lobby> lobbiesById = new ConcurrentDictionary<Guid, Lobby>();
ConcurrentDictionary<Guid, List<Lobby>> lobbiesByGameId = new ConcurrentDictionary<Guid, List<Lobby>>();
ConcurrentDictionary<int, Lobby> lobbiesByClientId = new ConcurrentDictionary<int, Lobby>();
ConcurrentDictionary<int, Guid> clientWatchingGameIdLobbies = new ConcurrentDictionary<int, Guid>();
ConcurrentDictionary<Guid, List<int>> clientsWatchingGameId = new ConcurrentDictionary<Guid, List<int>>();
BufferRental bufferRental = new BufferRental(4096);
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;
}
tcpServer.DataReceived += (clientId, dataLength, data) =>
{
if (dataLength > 0)
{
switch (PeekTypeId(data.Span))
{
case LobbyCreate.TypeId:
{
var lobbyCreate = LobbyCreate.Deserialize(data.Span);
if (lobbyCreate != null)
{
if (!GameGuids.ValidGuids.Contains(lobbyCreate.GameId))
throw new Exception("Invalid game guid!");
var lobby = new Lobby
{
Name = lobbyCreate.Name,
GameId = lobbyCreate.GameId,
GameMode = lobbyCreate.GameMode,
PlayerCount = lobbyCreate.PlayerCount,
MaxPlayerCount = lobbyCreate.MaxPlayerCount,
PasswordHash = lobbyCreate.PasswordHash,
HostClientId = clientId,
HostIp = lobbyCreate.HostIp == null ? tcpServer.GetClientIp(clientId)! : lobbyCreate.HostIp,
HostPort = lobbyCreate.HostPort,
};
if(lobbiesByClientId.TryGetValue(clientId, out var existingLobby))
{
var lobbyDelete = LobbyDelete.Deserialize(data.Span);
if (lobbyDelete != null)
{
if (lobbiesByClientId.TryRemove(clientId, out var _))
{
if (lobbiesById.TryRemove(existingLobby.Id, out var _))
{
if (lobbiesByGameId.TryGetValue(existingLobby.GameId, out var lobbyList))
{
lock (lobbyList)
{
lobbyList.Remove(existingLobby);
}
}
_ = Task.Run(() => SendLobbyUpdate(Lobby.LobbyUpdateType.Delete, existingLobby));
}
}
}
}
if (lobbiesByClientId.TryAdd(clientId, lobby))
{
lobbiesById.TryAdd(lobby.Id, lobby);
lobbiesByGameId.AddOrUpdate(lobby.GameId, new List<Lobby>() { lobby }, (key, lobbyList) =>
{
lock (lobbyList)
{
lobbyList.Add(lobby);
return lobbyList;
}
});
}
_ = Task.Run(() => SendLobbyUpdate(Lobby.LobbyUpdateType.Add, lobby));
}
}
break;
case LobbyUpdate.TypeId:
{
var lobbyUpdate = LobbyUpdate.Deserialize(data.Span);
if (lobbyUpdate != null)
{
if (lobbiesByClientId.TryGetValue(clientId, out var existingLobby))
{
existingLobby.Name = lobbyUpdate.Name;
existingLobby.GameMode = lobbyUpdate.GameMode;
existingLobby.PlayerCount = lobbyUpdate.PlayerCount;
existingLobby.MaxPlayerCount = lobbyUpdate.MaxPlayerCount;
existingLobby.PasswordHash = lobbyUpdate.PasswordHash;
if (lobbyUpdate.HostIp != null)
existingLobby.HostIp = lobbyUpdate.HostIp;
existingLobby.HostPort = lobbyUpdate.HostPort;
_ = Task.Run(() => SendLobbyUpdate(Lobby.LobbyUpdateType.Update, existingLobby));
}
}
}
break;
case LobbyDelete.TypeId:
{
var lobbyDelete = LobbyDelete.Deserialize(data.Span);
if (lobbyDelete != null)
{
if (lobbiesByClientId.TryRemove(clientId, out var existingLobby))
{
if (lobbiesById.TryRemove(existingLobby.Id, out var _))
{
if (lobbiesByGameId.TryGetValue(existingLobby.GameId, out var lobbyList))
{
lock (lobbyList)
{
lobbyList.Remove(existingLobby);
}
}
_ = Task.Run(() => SendLobbyUpdate(Lobby.LobbyUpdateType.Delete, existingLobby));
}
}
}
}
break;
case LobbiesObserve.TypeId:
{
var lobbyObserve = LobbiesObserve.Deserialize(data.Span);
if (lobbyObserve != null)
{
if (!GameGuids.ValidGuids.Contains(lobbyObserve.GameId))
throw new Exception("Invalid game guid!");
if (clientWatchingGameIdLobbies.TryAdd(clientId, lobbyObserve.GameId))
{
clientsWatchingGameId.AddOrUpdate(lobbyObserve.GameId, new List<int>() { clientId }, (key, clientWatchingLobbyList) =>
{
lock (clientWatchingLobbyList)
{
clientWatchingLobbyList.Add(clientId);
return clientWatchingLobbyList;
}
});
if (lobbiesByGameId.TryGetValue(lobbyObserve.GameId, out var lobbyList))
{
lock (lobbyList)
{
_ = Task.Run(() => SendLobbiesToClient(clientId, lobbyList.ToList()));
}
}
}
}
}
break;
case LobbiesStopObserve.TypeId:
{
var lobbyStopObserve = LobbiesStopObserve.Deserialize(data.Span);
if (lobbyStopObserve != null)
{
if (clientWatchingGameIdLobbies.TryRemove(clientId, out var gameGuid))
{
if (clientsWatchingGameId.TryGetValue(gameGuid, out var clientWatchingLobbyList))
{
lock (clientWatchingLobbyList)
{
clientWatchingLobbyList.Remove(clientId);
}
}
}
}
}
break;
case LobbyRequestHostInfo.TypeId:
{
var lobbyRequestHostInfo = LobbyRequestHostInfo.Deserialize(data.Span);
if(lobbyRequestHostInfo != null)
{
if(lobbiesById.TryGetValue(lobbyRequestHostInfo.LobbyId, out var lobby))
{
if(lobby.PasswordHash != null)
{
if(lobbyRequestHostInfo.PasswordHash == null || !lobby.PasswordHash.SequenceEqual(lobbyRequestHostInfo.PasswordHash))
{
var messageData = bufferRental.Rent();
var lobbyWrongPassword = new LobbyWrongPassword() { LobbyId = lobby.Id };
var messageDataLength = lobbyWrongPassword.Serialize(messageData);
_ = Task.Run(async () => {
await tcpServer.Send(clientId, messageData, 0, messageDataLength);
bufferRental.Return(messageData);
});
return;
}
}
{
var messageData = bufferRental.Rent();
var lobbyHostInfo = new LobbyHostInfo() { LobbyId = lobby.Id, HostIp = lobby.HostIp, HostPort = lobby.HostPort };
var messageDataLength = lobbyHostInfo.Serialize(messageData);
_ = Task.Run(async () =>
{
await tcpServer.Send(clientId, messageData, 0, messageDataLength);
bufferRental.Return(messageData);
});
}
}
}
}
break;
case LobbyRequestNatPunch.TypeId:
{
var lobbyRequestNatPunch = LobbyRequestNatPunch.Deserialize(data.Span);
if (lobbyRequestNatPunch != null)
{
if (lobbiesById.TryGetValue(lobbyRequestNatPunch.LobbyId, out var lobby))
{
if (lobby.PasswordHash != null)
{
if (lobbyRequestNatPunch.PasswordHash == null || !lobby.PasswordHash.SequenceEqual(lobbyRequestNatPunch.PasswordHash))
{
var messageData = bufferRental.Rent();
var lobbyWrongPassword = new LobbyWrongPassword() { LobbyId = lobby.Id };
var messageDataLength = lobbyWrongPassword.Serialize(messageData);
_ = Task.Run(async () => {
await tcpServer.Send(clientId, messageData, 0, messageDataLength);
bufferRental.Return(messageData);
});
return;
}
}
{
if (string.IsNullOrEmpty(lobbyRequestNatPunch.ClientIp))
{
lobbyRequestNatPunch.ClientIp = tcpServer.GetClientIp(clientId)!;
}
lobbyRequestNatPunch.NatPunchId = clientId;
var messageData = bufferRental.Rent();
var messageDataLength = lobbyRequestNatPunch.Serialize(messageData);
_ = Task.Run(async () =>
{
await tcpServer.Send(lobby.HostClientId, messageData, 0, messageDataLength);
bufferRental.Return(messageData);
});
}
}
}
}
break;
case LobbyNatPunchDone.TypeId:
{
var lobbyNatPunchDone = LobbyNatPunchDone.Deserialize(data.Span);
if (lobbyNatPunchDone != null)
{
if (lobbiesByClientId.TryGetValue(clientId, out var lobby))
{
lobbyNatPunchDone.LobbyId = lobby.Id;
var messageData = bufferRental.Rent();
var messageDataLength = lobbyNatPunchDone.Serialize(messageData);
_ = Task.Run(async () =>
{
await tcpServer.Send(lobbyNatPunchDone.NatPunchId, messageData, 0, messageDataLength);
bufferRental.Return(messageData);
});
}
}
}
break;
}
}
};
tcpServer.ClientDisconnected += (clientId) =>
{
if(clientWatchingGameIdLobbies.TryRemove(clientId, out var gameId))
{
if(clientsWatchingGameId.TryGetValue(gameId, out var clientWatchingLobbyList))
{
lock(clientWatchingLobbyList)
{
clientWatchingLobbyList.Remove(clientId);
}
}
}
if (lobbiesByClientId.TryRemove(clientId, out var lobby))
{
lobbiesById.TryRemove(lobby.Id, out var _);
if(lobbiesByGameId.TryGetValue(lobby.GameId, out var lobbyList))
{
lock(lobbyList)
{
lobbyList.Remove(lobby);
}
}
_ = Task.Run(() => SendLobbyUpdate(Lobby.LobbyUpdateType.Delete, lobby));
if (clientsWatchingGameId.TryGetValue(gameId, out var clientWatchingLobbyList))
{
lock (clientWatchingLobbyList)
{
clientWatchingLobbyList.Remove(clientId);
}
}
}
};
async Task SendLobbyUpdate(Lobby.LobbyUpdateType lobbyUpdateType, Lobby lobby)
{
byte[] messageData = bufferRental.Rent();
try
{
if (clientsWatchingGameId.TryGetValue(lobby.GameId, out var clientWatchingLobbyList))
{
int messageDataLength = 0;
switch (lobbyUpdateType)
{
case Lobby.LobbyUpdateType.Add:
case Lobby.LobbyUpdateType.Update:
var lobbyInfo = new LobbyInfo
{
Id = lobby.Id,
Name = lobby.Name,
GameMode = lobby.GameMode,
MaxPlayerCount = lobby.MaxPlayerCount,
PlayerCount = lobby.PlayerCount,
PasswordProtected = lobby.PasswordHash != null && lobby.PasswordHash.Length > 0
};
messageDataLength = lobbyInfo.Serialize(messageData);
break;
case Lobby.LobbyUpdateType.Delete:
var lobbyDelete = new LobbyDelete { Id = lobby.Id, GameId = lobby.GameId };
messageDataLength = lobbyDelete.Serialize(messageData);
break;
}
List<Task> tasks = new List<Task>();
if (messageDataLength != 0)
{
lock (clientWatchingLobbyList)
{
foreach (var watchingClientId in clientWatchingLobbyList)
{
tasks.Add(tcpServer.Send(watchingClientId, messageData, 0, messageDataLength));
}
}
}
await Task.WhenAll(tasks);
}
}
finally
{
bufferRental.Return(messageData);
}
}
async Task SendLobbiesToClient(int clientId, List<Lobby> lobbies)
{
byte[] messageData = bufferRental.Rent();
try
{
foreach (var lobby in lobbies)
{
var lobbyInfo = new LobbyInfo
{
Id = lobby.Id,
Name = lobby.Name,
GameMode = lobby.GameMode,
MaxPlayerCount = lobby.MaxPlayerCount,
PlayerCount = lobby.PlayerCount,
PasswordProtected = lobby.PasswordHash != null && lobby.PasswordHash.Length > 0
};
var messageDataLength = lobbyInfo.Serialize(messageData);
await tcpServer.Send(clientId, messageData, 0, messageDataLength);
}
}
finally
{
bufferRental.Return(messageData);
}
}
Console.CancelKeyPress += (sender, args) =>
{
Console.WriteLine($"{DateTime.Now}: Closing application");
tcpServer.Stop();
closing.Set();
args.Cancel = true;
};
Console.WriteLine($"{DateTime.Now}: Application started");
tcpServer.Start(8088);
closing.WaitOne();
Console.WriteLine($"{DateTime.Now}: Application closed");

View File

@ -0,0 +1,227 @@
using System.Net.Sockets;
using System.Net;
using System.Collections.Concurrent;
namespace LobbyServer
{
internal class TcpServer : IDisposable
{
public delegate void DataReceivedEventArgs(int clientId, int bytes, Memory<byte> data);
public event DataReceivedEventArgs? DataReceived;
public delegate void ClientDisconnectedEventArgs(int clientId);
public event ClientDisconnectedEventArgs? ClientDisconnected;
internal class Client : IDisposable
{
internal CancellationTokenSource? cancellationToken = null;
internal NetworkStream? stream;
internal TcpClient? client;
public void Dispose()
{
cancellationToken?.Dispose();
cancellationToken = null;
}
}
private int clientIdCounter = 0;
private bool running = false;
private readonly ConcurrentDictionary<int, Client> activeClients = new ConcurrentDictionary<int, Client> ();
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
private readonly AutoResetEvent serverClosed = new AutoResetEvent (false);
public void Start(int port)
{
serverClosed.Reset();
cancellationToken.TryReset();
running = true;
clientIdCounter = 0;
_ = Task.Run(() => Listener(port));
}
public void Stop()
{
running = false;
cancellationToken.Cancel();
foreach(var client in activeClients.Values)
{
try
{
client.cancellationToken?.Cancel();
}
catch
{ }
}
activeClients.Clear ();
serverClosed.WaitOne();
}
private async Task Listener(int port)
{
var ipEndPoint = new IPEndPoint(IPAddress.Any, port);
TcpListener listener = new(ipEndPoint);
try
{
cancellationToken.Token.ThrowIfCancellationRequested();
listener.Start();
Console.WriteLine($"{DateTime.Now}: Server listening on port {port}");
while (running)
{
var client = await listener.AcceptTcpClientAsync(cancellationToken.Token);
if (client != null && running)
{
_ = Task.Run(() => ClientThread(client));
}
}
}
finally
{
Console.WriteLine($"{DateTime.Now}: Server closed");
listener.Stop();
serverClosed.Set();
}
}
public async Task Send(int clientId, byte[] buffer, int offset, int count)
{
try
{
if (activeClients.TryGetValue(clientId, out var lobbyClient) && lobbyClient.stream != null && lobbyClient.cancellationToken != null)
{
await lobbyClient.stream.WriteAsync(BitConverter.GetBytes(count - offset), 0, 4, lobbyClient.cancellationToken.Token);
await lobbyClient.stream.WriteAsync(buffer, offset, count, lobbyClient.cancellationToken.Token);
}
}
catch { }
}
private async Task ClientThread(TcpClient client)
{
int myId = Interlocked.Increment(ref clientIdCounter);
try
{
await using NetworkStream stream = client.GetStream();
Memory<byte> buffer = new byte[4096];
Memory<byte> target = new byte[4096];
using var lobbyClient = new Client
{
cancellationToken = new CancellationTokenSource(),
stream = stream,
client = client
};
activeClients.TryAdd(myId, lobbyClient);
int currentOffset = 0;
int currentMessageRemainingLength = 0;
int currentMessageLength = 0;
bool validMessage = true;
bool offsetSizeInt = false;
int currentReadOffset = 0;
while (running)
{
int copyOffset = 0;
int receivedBytes = currentReadOffset;
if (currentReadOffset < 4)
{
receivedBytes += await stream.ReadAsync(buffer.Slice(currentReadOffset), lobbyClient.cancellationToken.Token) + currentReadOffset;
if (receivedBytes == 0)
throw new Exception("Connection lost!");
}
if (receivedBytes > 3 || (currentMessageRemainingLength > 0 && receivedBytes >= currentMessageRemainingLength))
{
currentReadOffset = 0;
if (currentMessageLength == 0)
{
currentMessageRemainingLength = BitConverter.ToInt32(buffer.Span);
currentMessageLength = currentMessageRemainingLength;
receivedBytes -= 4;
copyOffset += 4;
offsetSizeInt = true;
}
else
offsetSizeInt = false;
var receivedCount = Math.Min(receivedBytes, currentMessageRemainingLength);
receivedBytes -= receivedCount;
copyOffset += receivedCount;
if (validMessage && currentOffset + receivedCount > 0)
{
if (currentOffset + receivedCount < target.Length)
buffer.Slice(offsetSizeInt ? 4 : 0, receivedCount).CopyTo(target.Slice(currentOffset));
else
validMessage = false;
}
currentOffset += receivedCount;
currentMessageRemainingLength -= receivedCount;
if (currentMessageRemainingLength <= 0)
{
if(validMessage)
DataReceived?.Invoke(myId, currentMessageLength, target);
if (receivedBytes > 0)
{
buffer.Slice(copyOffset, receivedBytes).CopyTo(buffer);
currentReadOffset += receivedBytes;
}
currentOffset = 0;
currentMessageLength = 0;
currentMessageRemainingLength = 0;
validMessage = true;
}
}
else if(receivedBytes > 0)
{
currentReadOffset += receivedBytes;
}
}
}
finally
{
ClientDisconnected?.Invoke(myId);
activeClients.TryRemove(myId, out var _);
client?.Dispose();
}
}
internal string? GetClientIp(int cliendId)
{
try
{
if (activeClients.TryGetValue(cliendId, out var client))
{
return (client.client!.Client.RemoteEndPoint as IPEndPoint)!.Address.ToString();
}
}
catch { }
return null;
}
public void Dispose()
{
cancellationToken?.Dispose();
serverClosed?.Dispose();
}
}
}

View File

@ -0,0 +1,28 @@
using System.Collections.Concurrent;
using System.Drawing;
namespace LobbyServerDto
{
public class BufferRental
{
private readonly ConcurrentQueue<byte[]> buffers = new ConcurrentQueue<byte[]>();
private readonly int size;
public BufferRental(int size)
{
this.size = size;
}
public byte[] Rent()
{
if (buffers.TryDequeue(out var buffer))
return buffer;
else
return new byte[size];
}
public void Return(byte[] buffer)
{
buffers.Enqueue(buffer);
}
}
}

View File

@ -0,0 +1,8 @@
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbiesObserve
{
public Guid GameId { get; set; }
}
}

View File

@ -0,0 +1,7 @@
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbiesStopObserve
{
}
}

View File

@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyCreate
{
public Guid GameId { get; set; }
[MaxLength(64)]
public string Name { get; set; } = string.Empty;
public int GameMode { get; set; }
public int PlayerCount { get; set; }
public int MaxPlayerCount { get; set; }
[MaxLength(26)]
public byte[]? PasswordHash { get; set; }
[MaxLength(32)]
public string? HostIp { get; set; }
public int HostPort { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyDelete
{
public Guid Id { get; set; }
public Guid GameId { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyHostInfo
{
public Guid LobbyId { get; set; }
[MaxLength(32)]
public string? HostIp { get; set; }
public int HostPort { get; set; }
}
}

View File

@ -0,0 +1,13 @@
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyInfo
{
public Guid Id { get; set; }
public string? Name { get; set; }
public int GameMode { get; set; }
public int PlayerCount { get; set; }
public int MaxPlayerCount { get; set; }
public bool PasswordProtected { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyNatPunchDone
{
public Guid LobbyId { get; set; }
public int NatPunchId { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyRequestHostInfo
{
public Guid LobbyId { get; set; }
[MaxLength(26)]
public byte[]? PasswordHash { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyRequestNatPunch
{
public Guid LobbyId { get; set; }
[MaxLength(26)]
public byte[]? PasswordHash { get; set; }
public string? ClientIp { get; set; }
public int ClientPort { get; set; }
public int NatPunchId { get; set; }
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TargetFramework>net7.0</TargetFramework>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\LobbyServerSourceGenerator\LobbyServerSourceGenerator.csproj" OutputItemType="Analyzer" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations;
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyUpdate
{
public string Name { get; set; } = string.Empty;
public int GameMode { get; set; }
public int PlayerCount { get; set; }
public int MaxPlayerCount { get; set; }
public byte[]? PasswordHash { get; set; }
[MaxLength(32)]
public string? HostIp { get; set; }
public int HostPort { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace LobbyServerDto
{
[LobbyMessage]
public partial class LobbyWrongPassword
{
public Guid LobbyId { get; set; }
}
}

View File

@ -0,0 +1,29 @@
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 @@
{}

View File

@ -0,0 +1,9 @@

namespace LobbyServerDto
{
public static class GameGuids
{
public static Guid NFS = new Guid("0572706f-0fd9-46a9-8ea6-5a31a5363442");
public static HashSet<Guid> ValidGuids = new HashSet<Guid>() { NFS };
}
}

View File

@ -0,0 +1,342 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace LobbyServerDto
{
[Generator]
public class LobbyMessageSourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var typeDeclarations = context.SyntaxProvider.ForAttributeWithMetadataName(
"LobbyServerDto.LobbyMessageAttribute",
predicate: static (node, token) =>
{
return (node is ClassDeclarationSyntax
or StructDeclarationSyntax
or RecordDeclarationSyntax
or InterfaceDeclarationSyntax);
},
transform: static (context, token) =>
{
return (TypeDeclarationSyntax)context.TargetNode;
});
var source = typeDeclarations
.Combine(context.CompilationProvider)
.WithComparer(Comparer.Instance).Collect();
context.RegisterSourceOutput(source, static (context, source) =>
{
GenerateCode(context, source);
});
}
private static void GenerateCode(SourceProductionContext context, ImmutableArray<(TypeDeclarationSyntax, Compilation)> source)
{
if (source.IsDefaultOrEmpty)
return;
Dictionary<string, SyntaxInfo> sortableSyntaxes = new Dictionary<string, SyntaxInfo>();
foreach (var syntax in source)
{
var semanticModel = syntax.Item2.GetSemanticModel(syntax.Item1.SyntaxTree);
var typeSymbol = semanticModel.GetDeclaredSymbol(syntax.Item1, context.CancellationToken);
if (typeSymbol == null)
{
return;
}
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
? null
: typeSymbol.ContainingNamespace.ToString();
var name = typeSymbol.Name;
var fullName = $"{ns}.{name}";
sortableSyntaxes.Add(fullName, new SyntaxInfo() { name = name, nameSpace = ns, semanticModel = semanticModel, syntaxNode = syntax.Item1, compilation = syntax.Item2 });
}
StringBuilder s = new StringBuilder();
int id = 0;
foreach (var foundClass in sortableSyntaxes.OrderBy(s => s.Key))
{
s.Clear();
s.Append(@$"// <auto-generated />
using System.Collections.Generic;
using System.Text;
{(foundClass.Value.nameSpace is null ? null : $@"namespace {foundClass.Value.nameSpace}
{{")}
public partial class {foundClass.Value.name}
{{
public const int TypeId = {id++};
public int Serialize(byte[] buffer)
{{
int offset = 0;
{{
uint v = (uint)TypeId;
while (v >= 0x80)
{{
buffer[offset++] = (byte)(v | 0x80);
v >>= 7;
}}
buffer[offset++] = (byte)v;
}}
");
foreach (var member in foundClass.Value.syntaxNode.Members)
{
if (member is PropertyDeclarationSyntax p)
{
int maxLength = 256;
var iMember = foundClass.Value.semanticModel.GetDeclaredSymbol(member);
foreach (var maxLengthAttr in iMember.GetAttributes().Where(a => a.AttributeClass.Name == "MaxLengthAttribute"))
{
var length = (int)maxLengthAttr.ConstructorArguments.First().Value;
if(length < maxLength)
maxLength = length;
}
var name = p.Identifier.ToString();
switch(p.Type.ToString())
{
case "bool":
s.Append($@"
buffer[offset++] = (byte)({name} == true ? 1 : 0);");
break;
case "int":
s.Append($@"
buffer[offset++] = (byte){name};
buffer[offset++] = (byte)({name} >> 8);
buffer[offset++] = (byte)({name} >> 16);
buffer[offset++] = (byte)({name} >> 24);");
break;
case "Guid":
s.Append($@"
Buffer.BlockCopy({name}.ToByteArray(), 0, buffer, offset, 16);
offset += 16;");
break;
case "string":
case "string?":
s.Append($@"
if ({name} != null)
{{
var str1 = Encoding.UTF8.GetBytes({name}.Substring(0, Math.Min({maxLength}, {name}.Length)));
uint v = (uint)str1.Length;
while (v >= 0x80)
{{
buffer[offset++] = (byte)(v | 0x80);
v >>= 7;
}}
buffer[offset++] = (byte)v;
Buffer.BlockCopy(str1, 0, buffer, offset, str1.Length);
offset += str1.Length;
}}
else
{{
buffer[offset++] = 0;
}}
");
break;
case "byte[]":
case "byte[]?":
s.Append($@"
if ({name} != null)
{{
int maxLength = Math.Min(PasswordHash.Length, {maxLength});
uint v = (uint)maxLength;
while (v >= 0x80)
{{
buffer[offset++] = (byte)(v | 0x80);
v >>= 7;
}}
buffer[offset++] = (byte)v;
Buffer.BlockCopy(PasswordHash, 0, buffer, offset, maxLength);
offset += maxLength;
}}
else
{{
buffer[offset++] = 0;
}}
");
break;
default:
throw new Exception($"Unkown type {p.Type.ToString()} on field {name}");
}
}
}
s.Append(@$"
return offset;
}}
public static {foundClass.Value.name} Deserialize(ReadOnlySpan<byte> buffer)
{{
int offset = 0;
{foundClass.Value.name} ret = new {foundClass.Value.name}();
{{
int count = 0;
int shift = 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++];
count |= (b & 0x7F) << shift;
shift += 7;
}} while ((b & 0x80) != 0);
}}
");
foreach (var member in foundClass.Value.syntaxNode.Members)
{
if (member is PropertyDeclarationSyntax p)
{
int maxLength = 256;
var iMember = foundClass.Value.semanticModel.GetDeclaredSymbol(member);
foreach (var maxLengthAttr in iMember.GetAttributes().Where(a => a.AttributeClass.Name == "MaxLengthAttribute"))
{
var length = (int)maxLengthAttr.ConstructorArguments.First().Value;
if (length < maxLength)
maxLength = length;
}
var name = p.Identifier.ToString();
switch (p.Type.ToString())
{
case "bool":
s.Append($@"
ret.{name} = buffer[offset++] == 0 ? false : true;");
break;
case "int":
s.Append($@"
ret.{name} = (int)(buffer[offset++] | buffer[offset++] << 8 | buffer[offset++] << 16 | buffer[offset++] << 24);");
break;
case "Guid":
s.Append($@"
{{
ret.{name} = new Guid(buffer.Slice(offset, 16));
offset+=16;
}}");
break;
case "string":
case "string?":
s.Append($@"
{{
int strLen = 0;
int shift = 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++];
strLen |= (b & 0x7F) << shift;
shift += 7;
}} while ((b & 0x80) != 0);
if(strLen > 0)
{{
ret.{name} = Encoding.UTF8.GetString(buffer.Slice(offset, strLen));
offset += strLen;
}}
}}");
break;
case "byte[]":
case "byte[]?":
s.Append($@"
{{
int strLen = 0;
int shift = 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++];
strLen |= (b & 0x7F) << shift;
shift += 7;
}} while ((b & 0x80) != 0);
if(strLen > 0)
{{
ret.{name} = buffer.Slice(offset, strLen).ToArray();
offset += strLen;
}}
}}");
break;
default:
throw new Exception($"Unkown type {p.Type.ToString()} on field {name}");
}
}
}
s.Append(@$"
return ret;
}}
}}
{(foundClass.Value.nameSpace is null ? null : @"}
")}");
context.AddSource($"{foundClass.Key}.g.cs", s.ToString());
}
}
}
class SyntaxInfo
{
public TypeDeclarationSyntax syntaxNode;
public SemanticModel semanticModel;
public string nameSpace;
public string name;
internal Compilation compilation;
}
class Comparer : IEqualityComparer<(TypeDeclarationSyntax, Compilation)>
{
public static readonly Comparer Instance = new Comparer();
public bool Equals((TypeDeclarationSyntax, Compilation) x, (TypeDeclarationSyntax, Compilation) y)
{
return x.Item1.Equals(y.Item1);
}
public int GetHashCode((TypeDeclarationSyntax, Compilation) obj)
{
return obj.Item1.GetHashCode();
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class LobbyMessageAttribute : Attribute
{
}
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.7.0" />
</ItemGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,8 @@
{
"profiles": {
"Generator": {
"commandName": "DebugRoslynComponent",
"targetProject": "..\\LobbyServerDto\\LobbyServerDto.csproj"
}
}
}