0
0
Fork 0
mirror of https://github.com/Sanae6/SmoOnlineServer.git synced 2024-11-25 04:35:18 +00:00
SmoOnlineServer/Server/Server.cs

428 lines
20 KiB
C#
Raw Normal View History

2022-02-04 09:45:38 +00:00
using System.Buffers;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using Shared;
using Shared.Packet;
using Shared.Packet.Packets;
namespace Server;
public class Server {
public readonly List<Client> Clients = new List<Client>();
public IEnumerable<Client> ClientsConnected => Clients.Where(client => client.Metadata.ContainsKey("lastGamePacket") && client.Connected);
2022-02-04 09:45:38 +00:00
public readonly Logger Logger = new Logger("Server");
2022-02-10 01:44:50 +00:00
private readonly MemoryPool<byte> memoryPool = MemoryPool<byte>.Shared;
2022-02-16 00:35:38 +00:00
public Func<Client, IPacket, bool>? PacketHandler = null!;
public event Action<Client, ConnectPacket> ClientJoined = null!;
2022-02-10 01:44:50 +00:00
2022-02-22 04:05:13 +00:00
public async Task Listen(CancellationToken? token = null) {
2022-02-04 09:45:38 +00:00
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
2022-04-03 02:58:57 +00:00
serverSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
serverSocket.Bind(new IPEndPoint(IPAddress.Parse(Settings.Instance.Server.Address), Settings.Instance.Server.Port));
2022-02-04 09:45:38 +00:00
serverSocket.Listen();
2022-02-10 01:44:50 +00:00
Logger.Info($"Listening on {serverSocket.LocalEndPoint}");
2022-02-04 09:45:38 +00:00
2022-02-22 04:05:13 +00:00
try {
while (true) {
Socket socket = token.HasValue ? await serverSocket.AcceptAsync(token.Value) : await serverSocket.AcceptAsync();
2022-04-03 02:14:35 +00:00
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
2022-02-10 01:44:50 +00:00
2022-02-22 04:05:13 +00:00
Logger.Warn($"Accepted connection for client {socket.RemoteEndPoint}");
2022-02-04 09:45:38 +00:00
// start sub thread to handle client
2022-02-22 04:05:13 +00:00
try {
#pragma warning disable CS4014
Task.Run(() => HandleSocket(socket))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
}
2022-03-12 02:29:17 +00:00
catch (Exception e) {
Logger.Error($"Error occured while setting up socket handler? {e}");
2022-02-22 04:05:13 +00:00
}
}
}
catch (OperationCanceledException) {
2022-02-22 04:05:13 +00:00
// ignore the exception, it's just for closing the server
Logger.Info("Server closing");
2022-02-22 04:05:13 +00:00
try {
serverSocket.Shutdown(SocketShutdown.Both);
2022-03-22 09:20:49 +00:00
}
catch {
// ignored
2022-03-22 09:20:49 +00:00
}
finally {
serverSocket.Close();
}
Logger.Info("Server closed");
2022-07-28 05:56:50 +00:00
Console.WriteLine("\n\n\n"); //for the sake of the restart command.
}
2022-02-04 09:45:38 +00:00
}
2022-02-16 00:35:38 +00:00
public static void FillPacket<T>(PacketHeader header, T packet, Memory<byte> memory) where T : struct, IPacket {
2022-02-04 09:45:38 +00:00
Span<byte> data = memory.Span;
header.Serialize(data[..Constants.HeaderSize]);
2022-02-14 22:07:42 +00:00
packet.Serialize(data[Constants.HeaderSize..]);
2022-02-04 09:45:38 +00:00
}
// broadcast packets to all clients
public delegate void PacketReplacer<in T>(Client from, Client to, T value); // replacer must send
2022-04-27 20:02:26 +00:00
public void BroadcastReplace<T>(T packet, Client sender, PacketReplacer<T> packetReplacer) where T : struct, IPacket {
foreach (Client client in Clients.Where(c => c.Connected && !c.Ignored && sender.Id != c.Id)) {
packetReplacer(sender, client, packet);
}
}
2022-02-16 00:35:38 +00:00
public async Task Broadcast<T>(T packet, Client sender) where T : struct, IPacket {
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
2022-02-04 09:45:38 +00:00
PacketHeader header = new PacketHeader {
Id = sender?.Id ?? Guid.Empty,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size,
2022-02-04 09:45:38 +00:00
};
FillPacket(header, packet, memory.Memory);
await Broadcast(memory, sender);
}
public Task Broadcast<T>(T packet) where T : struct, IPacket {
return Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored).Select(async client => {
IMemoryOwner<byte> memory = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + packet.Size);
PacketHeader header = new PacketHeader {
Id = client.Id,
Type = Constants.PacketMap[typeof(T)].Type,
PacketSize = packet.Size,
};
FillPacket(header, packet, memory.Memory);
await client.Send(memory.Memory, client);
memory.Dispose();
}));
}
2022-02-04 09:45:38 +00:00
/// <summary>
2022-02-10 01:44:50 +00:00
/// Takes ownership of data and disposes once done.
2022-02-04 09:45:38 +00:00
/// </summary>
/// <param name="data">Memory owner to dispose once done</param>
/// <param name="sender">Optional sender to not broadcast data to</param>
public async Task Broadcast(IMemoryOwner<byte> data, Client? sender = null) {
await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data.Memory, sender)));
2022-02-04 09:45:38 +00:00
data.Dispose();
}
/// <summary>
2022-02-10 01:44:50 +00:00
/// Broadcasts memory whose memory shouldn't be disposed, should only be fired by server code.
2022-02-04 09:45:38 +00:00
/// </summary>
/// <param name="data">Memory to send to the clients</param>
/// <param name="sender">Optional sender to not broadcast data to</param>
public async void Broadcast(Memory<byte> data, Client? sender = null) {
await Task.WhenAll(Clients.Where(c => c.Connected && !c.Ignored && c != sender).Select(client => client.Send(data, sender)));
2022-02-04 09:45:38 +00:00
}
public Client? FindExistingClient(Guid id) {
return Clients.Find(client => client.Id == id);
}
private async void HandleSocket(Socket socket) {
Client client = new Client(socket) {Server = this};
var remote = socket.RemoteEndPoint;
2022-02-04 09:45:38 +00:00
IMemoryOwner<byte> memory = null!;
2022-02-04 09:45:38 +00:00
bool first = true;
try {
while (true) {
memory = memoryPool.Rent(Constants.HeaderSize);
2022-03-22 09:20:49 +00:00
async Task<bool> Read(Memory<byte> readMem, int readSize, int readOffset) {
readSize += readOffset;
while (readOffset < readSize) {
int size = await socket.ReceiveAsync(readMem[readOffset..readSize], SocketFlags.None);
2022-02-15 21:17:18 +00:00
if (size == 0) {
// treat it as a disconnect and exit
Logger.Info($"Socket {remote} disconnected.");
2022-02-15 21:17:18 +00:00
if (socket.Connected) await socket.DisconnectAsync(false);
return false;
2022-02-15 21:17:18 +00:00
}
readOffset += size;
}
return true;
2022-02-04 09:45:38 +00:00
}
if (!await Read(memory.Memory[..Constants.HeaderSize], Constants.HeaderSize, 0)) {
break;
}
PacketHeader header = GetHeader(memory.Memory.Span[..Constants.HeaderSize]);
Range packetRange = Constants.HeaderSize..(Constants.HeaderSize + header.PacketSize);
if (header.PacketSize > 0) {
IMemoryOwner<byte> memTemp = memory; // header to copy to new memory
memory = memoryPool.Rent(Constants.HeaderSize + header.PacketSize);
memTemp.Memory.Span[..Constants.HeaderSize].CopyTo(memory.Memory.Span[..Constants.HeaderSize]);
memTemp.Dispose();
if (!await Read(memory.Memory, header.PacketSize, Constants.HeaderSize)) {
break;
}
}
Refactoring ban command (#48) * rename command: `ban ...` => `ban player ...` To enable adding other subcommands starting with `ban`. Moving ban list and crash related code into its own class to tidy the Program class up. Change Id values of the crash cmds, to fit into the 16 byte max length imposed by ChangeStagePacket.IdSize. * add command: `ban ip <ipv4-address>` To add an IPv4 address to the ban list. * add command: `ban profile <profile-id>` To add a profile ID to the ban list. * add command: `unban ip <ipv4-address>` To remove a banned IPv4 address from the ban list. * add command: `unban profile <profile-id>` To remove a banned profile ID from the ban list. * add commands: `ban enable` and `ban disable` To set the value of `BanList.Enabled` to `true` or `false` without editing the `settings.json` file. * add command: `ban list` To show the current ban list settings. * fix: actually working ban functionality Changes: - ignore new sockets from banned IP addresses way earlier. - ignore all packets by banned profiles. Intentionally keeping the connection open instead of d/c banned clients. This is to prevent endless server logs due to automatically reconnecting clients. Before: Reconnecting clients aren't entering `ClientJoined` and therefore the d/c is only working on first connections. Effectively banned clients got a d/c and then automatically reconnected again without getting a d/c again. Therefore allowing them to play normally. * use SortedSet instead of List for settings To enforce unique entries and maintain a stable order inside of the `settings.json`. * add commands: `ban stage <stage-name>` and `unban stage <stage-name>` To kick players from the server when they enter a banned stage. <stage-name> can also be a kingdom alias, which bans/unbans all stages in that kingdom. Because we aren't banning the player, d/c them would be no good, because of the client auto reconnect. Instead send them the crash and ignore all packets by them until they d/c on their own. This is an alternative solution for issue #43. * Update Server.cs --------- Co-authored-by: Sanae <32604996+Sanae6@users.noreply.github.com>
2023-09-05 23:14:54 +00:00
2022-02-04 09:45:38 +00:00
// connection initialization
if (first) {
first = false; // only do this once
// first client packet has to be the client init
if (header.Type != PacketType.Connect) {
throw new Exception($"First packet was not init, instead it was {header.Type} ({remote})");
}
2022-02-04 09:45:38 +00:00
2022-02-14 20:40:42 +00:00
ConnectPacket connect = new ConnectPacket();
connect.Deserialize(memory.Memory.Span[packetRange]);
Refactoring ban command (#48) * rename command: `ban ...` => `ban player ...` To enable adding other subcommands starting with `ban`. Moving ban list and crash related code into its own class to tidy the Program class up. Change Id values of the crash cmds, to fit into the 16 byte max length imposed by ChangeStagePacket.IdSize. * add command: `ban ip <ipv4-address>` To add an IPv4 address to the ban list. * add command: `ban profile <profile-id>` To add a profile ID to the ban list. * add command: `unban ip <ipv4-address>` To remove a banned IPv4 address from the ban list. * add command: `unban profile <profile-id>` To remove a banned profile ID from the ban list. * add commands: `ban enable` and `ban disable` To set the value of `BanList.Enabled` to `true` or `false` without editing the `settings.json` file. * add command: `ban list` To show the current ban list settings. * fix: actually working ban functionality Changes: - ignore new sockets from banned IP addresses way earlier. - ignore all packets by banned profiles. Intentionally keeping the connection open instead of d/c banned clients. This is to prevent endless server logs due to automatically reconnecting clients. Before: Reconnecting clients aren't entering `ClientJoined` and therefore the d/c is only working on first connections. Effectively banned clients got a d/c and then automatically reconnected again without getting a d/c again. Therefore allowing them to play normally. * use SortedSet instead of List for settings To enforce unique entries and maintain a stable order inside of the `settings.json`. * add commands: `ban stage <stage-name>` and `unban stage <stage-name>` To kick players from the server when they enter a banned stage. <stage-name> can also be a kingdom alias, which bans/unbans all stages in that kingdom. Because we aren't banning the player, d/c them would be no good, because of the client auto reconnect. Instead send them the crash and ignore all packets by them until they d/c on their own. This is an alternative solution for issue #43. * Update Server.cs --------- Co-authored-by: Sanae <32604996+Sanae6@users.noreply.github.com>
2023-09-05 23:14:54 +00:00
client.Id = header.Id;
client.Name = connect.ClientName;
// is the IPv4 address banned?
if (BanLists.Enabled && BanLists.IsIPv4Banned(((IPEndPoint) socket.RemoteEndPoint!).Address!)) {
Logger.Warn($"Ignoring banned IPv4 address for {client.Name} ({client.Id}/{remote})");
client.Ignored = true;
client.Banned = true;
}
// is the profile ID banned?
else if (BanLists.Enabled && BanLists.IsProfileBanned(client.Id)) {
client.Logger.Warn($"Ignoring banned profile ID for {client.Name} ({client.Id}/{remote})");
Refactoring ban command (#48) * rename command: `ban ...` => `ban player ...` To enable adding other subcommands starting with `ban`. Moving ban list and crash related code into its own class to tidy the Program class up. Change Id values of the crash cmds, to fit into the 16 byte max length imposed by ChangeStagePacket.IdSize. * add command: `ban ip <ipv4-address>` To add an IPv4 address to the ban list. * add command: `ban profile <profile-id>` To add a profile ID to the ban list. * add command: `unban ip <ipv4-address>` To remove a banned IPv4 address from the ban list. * add command: `unban profile <profile-id>` To remove a banned profile ID from the ban list. * add commands: `ban enable` and `ban disable` To set the value of `BanList.Enabled` to `true` or `false` without editing the `settings.json` file. * add command: `ban list` To show the current ban list settings. * fix: actually working ban functionality Changes: - ignore new sockets from banned IP addresses way earlier. - ignore all packets by banned profiles. Intentionally keeping the connection open instead of d/c banned clients. This is to prevent endless server logs due to automatically reconnecting clients. Before: Reconnecting clients aren't entering `ClientJoined` and therefore the d/c is only working on first connections. Effectively banned clients got a d/c and then automatically reconnected again without getting a d/c again. Therefore allowing them to play normally. * use SortedSet instead of List for settings To enforce unique entries and maintain a stable order inside of the `settings.json`. * add commands: `ban stage <stage-name>` and `unban stage <stage-name>` To kick players from the server when they enter a banned stage. <stage-name> can also be a kingdom alias, which bans/unbans all stages in that kingdom. Because we aren't banning the player, d/c them would be no good, because of the client auto reconnect. Instead send them the crash and ignore all packets by them until they d/c on their own. This is an alternative solution for issue #43. * Update Server.cs --------- Co-authored-by: Sanae <32604996+Sanae6@users.noreply.github.com>
2023-09-05 23:14:54 +00:00
client.Ignored = true;
client.Banned = true;
Refactoring ban command (#48) * rename command: `ban ...` => `ban player ...` To enable adding other subcommands starting with `ban`. Moving ban list and crash related code into its own class to tidy the Program class up. Change Id values of the crash cmds, to fit into the 16 byte max length imposed by ChangeStagePacket.IdSize. * add command: `ban ip <ipv4-address>` To add an IPv4 address to the ban list. * add command: `ban profile <profile-id>` To add a profile ID to the ban list. * add command: `unban ip <ipv4-address>` To remove a banned IPv4 address from the ban list. * add command: `unban profile <profile-id>` To remove a banned profile ID from the ban list. * add commands: `ban enable` and `ban disable` To set the value of `BanList.Enabled` to `true` or `false` without editing the `settings.json` file. * add command: `ban list` To show the current ban list settings. * fix: actually working ban functionality Changes: - ignore new sockets from banned IP addresses way earlier. - ignore all packets by banned profiles. Intentionally keeping the connection open instead of d/c banned clients. This is to prevent endless server logs due to automatically reconnecting clients. Before: Reconnecting clients aren't entering `ClientJoined` and therefore the d/c is only working on first connections. Effectively banned clients got a d/c and then automatically reconnected again without getting a d/c again. Therefore allowing them to play normally. * use SortedSet instead of List for settings To enforce unique entries and maintain a stable order inside of the `settings.json`. * add commands: `ban stage <stage-name>` and `unban stage <stage-name>` To kick players from the server when they enter a banned stage. <stage-name> can also be a kingdom alias, which bans/unbans all stages in that kingdom. Because we aren't banning the player, d/c them would be no good, because of the client auto reconnect. Instead send them the crash and ignore all packets by them until they d/c on their own. This is an alternative solution for issue #43. * Update Server.cs --------- Co-authored-by: Sanae <32604996+Sanae6@users.noreply.github.com>
2023-09-05 23:14:54 +00:00
}
// is the server full?
else if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) {
client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}");
client.Ignored = true;
}
Refactoring ban command (#48) * rename command: `ban ...` => `ban player ...` To enable adding other subcommands starting with `ban`. Moving ban list and crash related code into its own class to tidy the Program class up. Change Id values of the crash cmds, to fit into the 16 byte max length imposed by ChangeStagePacket.IdSize. * add command: `ban ip <ipv4-address>` To add an IPv4 address to the ban list. * add command: `ban profile <profile-id>` To add a profile ID to the ban list. * add command: `unban ip <ipv4-address>` To remove a banned IPv4 address from the ban list. * add command: `unban profile <profile-id>` To remove a banned profile ID from the ban list. * add commands: `ban enable` and `ban disable` To set the value of `BanList.Enabled` to `true` or `false` without editing the `settings.json` file. * add command: `ban list` To show the current ban list settings. * fix: actually working ban functionality Changes: - ignore new sockets from banned IP addresses way earlier. - ignore all packets by banned profiles. Intentionally keeping the connection open instead of d/c banned clients. This is to prevent endless server logs due to automatically reconnecting clients. Before: Reconnecting clients aren't entering `ClientJoined` and therefore the d/c is only working on first connections. Effectively banned clients got a d/c and then automatically reconnected again without getting a d/c again. Therefore allowing them to play normally. * use SortedSet instead of List for settings To enforce unique entries and maintain a stable order inside of the `settings.json`. * add commands: `ban stage <stage-name>` and `unban stage <stage-name>` To kick players from the server when they enter a banned stage. <stage-name> can also be a kingdom alias, which bans/unbans all stages in that kingdom. Because we aren't banning the player, d/c them would be no good, because of the client auto reconnect. Instead send them the crash and ignore all packets by them until they d/c on their own. This is an alternative solution for issue #43. * Update Server.cs --------- Co-authored-by: Sanae <32604996+Sanae6@users.noreply.github.com>
2023-09-05 23:14:54 +00:00
// send server init (required to crash ignored players later)
await client.Send(new InitPacket {
MaxPlayers = (client.Ignored ? (ushort) 1 : Settings.Instance.Server.MaxPlayers),
});
// don't init or announce an ignored client to other players any further
if (client.Ignored) {
memory.Dispose();
continue;
}
bool wasFirst = connect.ConnectionType == ConnectPacket.ConnectionTypes.FirstConnection;
// add client to the set of connected players
2022-02-04 09:45:38 +00:00
lock (Clients) {
// is the server full? (check again, to prevent race conditions)
if (Clients.Count(x => x.Connected) >= Settings.Instance.Server.MaxPlayers) {
client.Logger.Error($"Ignoring player {client.Name} ({client.Id}/{remote}) as server reached max players of {Settings.Instance.Server.MaxPlayers}");
client.Ignored = true;
2022-03-22 09:17:48 +00:00
memory.Dispose();
continue;
2022-03-22 09:17:48 +00:00
}
2022-03-22 09:20:49 +00:00
// detect and handle reconnections
bool isClientNew = true;
2022-02-04 09:45:38 +00:00
switch (connect.ConnectionType) {
case ConnectPacket.ConnectionTypes.FirstConnection:
2022-03-13 11:14:01 +00:00
case ConnectPacket.ConnectionTypes.Reconnecting: {
if (FindExistingClient(client.Id) is { } oldClient) {
isClientNew = false;
client = new Client(oldClient, socket);
client.Name = connect.ClientName;
Clients.Remove(oldClient);
Clients.Add(client);
if (oldClient.Connected) {
oldClient.Logger.Info($"Disconnecting already connected client {oldClient.Socket?.RemoteEndPoint} for {client.Socket?.RemoteEndPoint}");
oldClient.Dispose();
}
}
else {
2022-03-13 11:14:01 +00:00
connect.ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection;
2022-02-04 09:45:38 +00:00
}
2022-02-10 01:44:50 +00:00
2022-02-04 09:45:38 +00:00
break;
}
default: {
throw new Exception($"Invalid connection type {connect.ConnectionType} for {client.Name} ({client.Id}/{remote})");
}
2022-02-04 09:45:38 +00:00
}
2022-02-09 21:56:57 +00:00
client.Connected = true;
if (isClientNew) {
2022-02-04 09:45:38 +00:00
// do any cleanup required when it comes to new clients
List<Client> toDisconnect = Clients.FindAll(c => c.Id == client.Id && c.Connected && c.Socket != null);
Clients.RemoveAll(c => c.Id == client.Id);
2022-02-10 01:44:50 +00:00
2022-02-04 09:45:38 +00:00
Clients.Add(client);
Parallel.ForEachAsync(toDisconnect, (c, token) => c.Socket!.DisconnectAsync(false, token));
// done disconnecting and removing stale clients with the same id
ClientJoined?.Invoke(client, connect);
}
// a known client reconnects, but with a new first connection (e.g. after a restart)
else if (wasFirst) {
client.CleanMetadataOnNewConnection();
2022-02-04 09:45:38 +00:00
}
}
2022-02-10 01:44:50 +00:00
// for all other clients that are already connected
List<Client> otherConnectedPlayers = Clients.FindAll(c => c.Id != client.Id && c.Connected && c.Socket != null);
await Parallel.ForEachAsync(otherConnectedPlayers, async (other, _) => {
IMemoryOwner<byte> tempBuffer = MemoryPool<byte>.Shared.RentZero(Constants.HeaderSize + (other.CurrentCostume.HasValue ? Math.Max(connect.Size, other.CurrentCostume.Value.Size) : connect.Size));
// make the other client known to the new client
2022-02-10 01:44:50 +00:00
PacketHeader connectHeader = new PacketHeader {
Id = other.Id,
Type = PacketType.Connect,
PacketSize = connect.Size,
};
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
2022-02-10 01:44:50 +00:00
ConnectPacket connectPacket = new ConnectPacket {
2022-03-13 11:14:01 +00:00
ConnectionType = ConnectPacket.ConnectionTypes.FirstConnection, // doesn't matter what it is
MaxPlayers = Settings.Instance.Server.MaxPlayers,
ClientName = other.Name,
};
2022-02-14 22:06:49 +00:00
connectPacket.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..]);
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connect.Size)], null);
// tell the new client what costume the other client has
2022-02-12 23:37:03 +00:00
if (other.CurrentCostume.HasValue) {
connectHeader.Type = PacketType.Costume;
connectHeader.PacketSize = other.CurrentCostume.Value.Size;
connectHeader.Serialize(tempBuffer.Memory.Span[..Constants.HeaderSize]);
other.CurrentCostume.Value.Serialize(tempBuffer.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + connectHeader.PacketSize)]);
await client.Send(tempBuffer.Memory[..(Constants.HeaderSize + connectHeader.PacketSize)], null);
2022-02-11 04:25:47 +00:00
}
2022-02-11 04:25:47 +00:00
tempBuffer.Dispose();
// make the other client reset their puppet cache for this new client, if it is a new connection (after restart)
if (wasFirst) {
await SendEmptyPackets(client, other);
}
});
2022-02-10 01:44:50 +00:00
Logger.Info($"Client {client.Name} ({client.Id}/{remote}) connected.");
// send missing or outdated packets from others to the new client
await ResendPackets(client);
}
else if (header.Id != client.Id && client.Id != Guid.Empty) {
throw new Exception($"Client {client.Name} sent packet with invalid client id {header.Id} instead of {client.Id}");
}
2022-02-04 09:45:38 +00:00
2022-02-14 21:17:36 +00:00
try {
// parse the packet
2022-02-14 20:40:42 +00:00
IPacket packet = (IPacket) Activator.CreateInstance(Constants.PacketIdMap[header.Type])!;
packet.Deserialize(memory.Memory.Span[Constants.HeaderSize..(Constants.HeaderSize + packet.Size)]);
// process the packet
if (PacketHandler?.Invoke(client, packet) is false) {
// don't broadcast the packet to everyone
memory.Dispose();
continue;
}
}
catch (Exception e) {
2022-02-16 00:35:38 +00:00
client.Logger.Error($"Packet handler warning: {e}");
2022-02-14 20:40:42 +00:00
}
#pragma warning disable CS4014
// broadcast the packet to everyone
Broadcast(memory, client)
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
2022-02-04 09:45:38 +00:00
}
}
catch (Exception e) {
if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) {
2022-03-12 02:16:10 +00:00
client.Logger.Info($"Disconnected from the server: Connection reset");
}
else {
2022-03-12 02:16:10 +00:00
client.Logger.Error($"Disconnecting due to exception: {e}");
if (socket.Connected) {
#pragma warning disable CS4014
Task.Run(() => socket.DisconnectAsync(false))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
#pragma warning restore CS4014
}
2022-02-04 09:45:38 +00:00
}
memory?.Dispose();
}
2022-03-22 09:20:49 +00:00
// client disconnected
if (client.Name != "Unknown User" && client.Id != Guid.Parse("00000000-0000-0000-0000-000000000000")) {
Logger.Info($"Client {remote} ({client.Name}/{client.Id}) disconnected from the server");
}
else {
Logger.Info($"Client {remote} disconnected from the server");
}
2022-02-04 09:45:38 +00:00
bool wasConnected = client.Connected;
2022-03-15 20:44:46 +00:00
client.Connected = false;
try {
client.Dispose();
2022-03-22 09:20:49 +00:00
}
2022-04-28 03:32:52 +00:00
catch { /*lol*/ }
2022-03-22 09:20:49 +00:00
#pragma warning disable CS4014
if (wasConnected) {
Task.Run(() => Broadcast(new DisconnectPacket(), client))
.ContinueWith(x => { if (x.Exception != null) { Logger.Error(x.Exception.ToString()); } });
}
#pragma warning restore CS4014
2022-02-04 09:45:38 +00:00
}
private async Task ResendPackets(Client client) {
async Task trySendPack<T>(Client other, T? packet) where T : struct, IPacket {
if (packet == null) { return; }
try {
await client.Send((T) packet, other);
}
catch {
// lol who gives a fuck
}
};
async Task trySendMeta<T>(Client other, string packetType) where T : struct, IPacket {
if (!other.Metadata.ContainsKey(packetType)) { return; }
await trySendPack<T>(other, (T) other.Metadata[packetType]!);
};
await Parallel.ForEachAsync(this.ClientsConnected, async (other, _) => {
if (client.Id == other.Id) { return; }
await trySendMeta<CostumePacket>(other, "lastCostumePacket");
await trySendMeta<CapturePacket>(other, "lastCapturePacket");
await trySendPack<TagPacket>(other, other.GetTagPacket());
await trySendMeta<GamePacket>(other, "lastGamePacket");
await trySendMeta<PlayerPacket>(other, "lastPlayerPacket");
});
}
private async Task SendEmptyPackets(Client client, Client other) {
await other.Send(new TagPacket {
UpdateType = TagPacket.TagUpdate.State | TagPacket.TagUpdate.Time,
IsIt = false,
Seconds = 0,
Minutes = 0,
}, client);
await other.Send(new CapturePacket {
ModelName = "",
}, client);
}
2022-02-04 09:45:38 +00:00
private static PacketHeader GetHeader(Span<byte> data) {
//no need to error check, the client will disconnect when the packet is invalid :)
PacketHeader header = new PacketHeader();
header.Deserialize(data[..Constants.HeaderSize]);
return header;
2022-02-04 09:45:38 +00:00
}
}