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