Projektdateien hinzufügen.
parent
cbbf47885d
commit
2674eb2a10
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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");
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace LobbyServerDto
|
||||
{
|
||||
[LobbyMessage]
|
||||
public partial class LobbiesObserve
|
||||
{
|
||||
public Guid GameId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace LobbyServerDto
|
||||
{
|
||||
[LobbyMessage]
|
||||
public partial class LobbiesStopObserve
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace LobbyServerDto
|
||||
{
|
||||
[LobbyMessage]
|
||||
public partial class LobbyDelete
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid GameId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
namespace LobbyServerDto
|
||||
{
|
||||
[LobbyMessage]
|
||||
public partial class LobbyWrongPassword
|
||||
{
|
||||
public Guid LobbyId { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"profiles": {
|
||||
"Generator": {
|
||||
"commandName": "DebugRoslynComponent",
|
||||
"targetProject": "..\\LobbyServerDto\\LobbyServerDto.csproj"
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue