commit 3f4a3fee9eef9f5932103ccec9bd26ed613874e6 Author: Sanae Date: Sun Nov 28 22:04:34 2021 -0600 Initial Commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39c9242 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +.idea/ diff --git a/Server/Client.cs b/Server/Client.cs new file mode 100644 index 0000000..1002404 --- /dev/null +++ b/Server/Client.cs @@ -0,0 +1,23 @@ +using System.Buffers; +using System.Net.Sockets; + +namespace Server; + +public class Client : IDisposable { + public Socket? Socket; + public bool Connected => Socket?.Connected ?? false; + public Guid Id; + public readonly Dictionary Metadata = new Dictionary(); // can be used to store any information about a player + + public async Task Send(Memory data) { + if (!Connected) return; + await Socket!.SendAsync(data, SocketFlags.None); + } + + public void Dispose() { + Socket?.Disconnect(false); + } + + public static bool operator ==(Client? left, Client? right) => left is { } leftClient && right is { } rightClient && leftClient.Id == rightClient.Id; + public static bool operator !=(Client? left, Client? right) => !(left == right); +} \ No newline at end of file diff --git a/Server/Program.cs b/Server/Program.cs new file mode 100644 index 0000000..a354723 --- /dev/null +++ b/Server/Program.cs @@ -0,0 +1,6 @@ +using System.Buffers; +using System.Net.Sockets; + +Server.Server server = new Server.Server(); + +await server.Listen(1027); \ No newline at end of file diff --git a/Server/Server.cs b/Server/Server.cs new file mode 100644 index 0000000..e5c5238 --- /dev/null +++ b/Server/Server.cs @@ -0,0 +1,162 @@ +using System.Buffers; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using Shared; +using Shared.Packet; +using Shared.Packet.Packets; + +namespace Server; + +public class Server { + private readonly MemoryPool memoryPool = MemoryPool.Shared; + public readonly List Clients = new List(); + public readonly Logger Logger = new Logger("Server"); + private static int HeaderSize => Marshal.SizeOf(); + public async Task Listen(ushort port) { + TcpListener listener = TcpListener.Create(port); + + listener.Start(); + + while (true) { + Socket socket = await listener.AcceptSocketAsync(); + + if (Clients.Count > Constants.MaxClients) { + Logger.Warn("Turned away client due to max clients"); + await socket.DisconnectAsync(false); + continue; + } + + HandleSocket(socket); + } + } + + public static void FillPacket(PacketHeader header, T packet, Memory memory) where T : unmanaged, IPacket { + Span data = memory.Span; + + MemoryMarshal.Write(data, ref header); + MemoryMarshal.Write(data[HeaderSize..], ref packet); + } + + // broadcast packets to all clients + public async void Broadcast(T packet, Client? sender = null) where T : unmanaged, IPacket { + IMemoryOwner memory = memoryPool.Rent(Marshal.SizeOf() + HeaderSize); + + PacketHeader header = new PacketHeader { + Id = sender?.Id ?? Guid.Empty, + Type = Constants.Packets[typeof(T)].Type, + Sender = PacketSender.Server // todo maybe use client? + }; + FillPacket(header, packet, memory.Memory); + await Broadcast(memory, sender); + } + + /// + /// Takes ownership of data and disposes once done. + /// + /// Memory owner to dispose once done + /// Optional sender to not broadcast data to + public async Task Broadcast(IMemoryOwner data, Client? sender = null) { + await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data.Memory))); + data.Dispose(); + } + + /// + /// Broadcasts memory whose memory shouldn't be disposed, should only be fired by server code. + /// + /// Memory to send to the clients + /// Optional sender to not broadcast data to + public async void Broadcast(Memory data, Client? sender = null) { + await Task.WhenAll(Clients.Where(c => c.Connected && c != sender).Select(client => client.Send(data))); + } + + public Client? FindExistingClient(Guid id) { + return Clients.Find(client => client.Id == id); + } + + + private async void HandleSocket(Socket socket) { + Client client = new Client {Socket = socket}; + IMemoryOwner memory = null!; + bool first = true; + try { + while (true) { + memory = memoryPool.Rent(Constants.MaxPacketSize); + int size = await socket.ReceiveAsync(memory.Memory, SocketFlags.None); + if (size == 0) { // treat it as a disconnect and exit + Logger.Info($"Socket {socket.RemoteEndPoint} disconnected."); + await socket.DisconnectAsync(false); + break; + } + + PacketHeader header = GetHeader(memory.Memory.Span[..size]); + //Logger.Info($"first = {first}, type = {header.Type}, data = " + memory.Memory.Span[..size].Hex()); + // connection initialization + if (first) { + first = false; + if (header.Type != PacketType.Connect) { + throw new Exception("First packet was not init"); + } + + ConnectPacket connect = MemoryMarshal.Read(memory.Memory.Span[HeaderSize..size]); + lock (Clients) { + switch (connect.ConnectionType) { + case ConnectionTypes.FirstConnection: { + // do any cleanup required when it comes to new clients + List toDisconnect = Clients.FindAll(c => c.Id == header.Id && c.Connected && c.Socket != null); + Clients.RemoveAll(c => c.Id == header.Id); + + client.Id = header.Id; + Clients.Add(client); + + foreach (Client c in toDisconnect) c.Socket!.DisconnectAsync(false); + // done disconnecting and removing stale clients with the same id + break; + } + case ConnectionTypes.Reconnecting: { + if (FindExistingClient(header.Id) is { } newClient) { + if (newClient.Connected) throw new Exception($"Tried to join as already connected user {header.Id}"); + newClient.Socket = client.Socket; + client = newClient; + } else { + // TODO MAJOR: IF CLIENT COULD NOT BE FOUND, SERVER SHOULD GRACEFULLY TELL CLIENT THAT ON DISCONNECT + // can be done via disconnect packet when that gets more data? + throw new Exception("Could not find a suitable client to reconnect as"); + } + break; + } + default: + throw new Exception($"Invalid connection type {connect.ConnectionType}"); + } + } + + Logger.Info($"Client {socket.RemoteEndPoint} connected."); + + continue; + } + + + // todo support variable length packets when they show up + if (header.Sender == PacketSender.Client) await Broadcast(memory, client); + else { + //todo handle server packets :) + } + } + } + catch (Exception e) { + if (e is SocketException {SocketErrorCode: SocketError.ConnectionReset}) { + Logger.Info($"Client {socket.RemoteEndPoint} disconnected from the server"); + } else { + Logger.Error($"Exception on socket {socket.RemoteEndPoint}, disconnecting for: {e}"); + await socket.DisconnectAsync(false); + } + + memory?.Dispose(); + } + client.Dispose(); + } + + private static PacketHeader GetHeader(Span data) { + //no need to error check, the client will disconnect when the packet is invalid :) + return MemoryMarshal.Read(data); + } +} \ No newline at end of file diff --git a/Server/Server.csproj b/Server/Server.csproj new file mode 100644 index 0000000..7d518c2 --- /dev/null +++ b/Server/Server.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + diff --git a/Shared/Constants.cs b/Shared/Constants.cs new file mode 100644 index 0000000..7dcd911 --- /dev/null +++ b/Shared/Constants.cs @@ -0,0 +1,17 @@ +using System.Reflection; +using Shared.Packet; +using Shared.Packet.Packets; + +namespace Shared; + +public static class Constants { + public const int MaxPacketSize = 256; + public const int MaxClients = 4; + + // dictionary of packet types to packet + public static readonly Dictionary Packets = Assembly + .GetExecutingAssembly() + .GetTypes() + .Where(type => type.IsAssignableTo(typeof(IPacket))) + .ToDictionary(type => type, type => type.GetCustomAttribute()!); +} \ No newline at end of file diff --git a/Shared/Extensions.cs b/Shared/Extensions.cs new file mode 100644 index 0000000..09e7351 --- /dev/null +++ b/Shared/Extensions.cs @@ -0,0 +1,11 @@ +using System.Text; + +namespace Shared; + +public static class Extensions { + public static string Hex(this Span span) { + return span.ToArray().Hex(); + } + + public static string Hex(this IEnumerable array) => string.Join(' ', array.ToArray().Select(x => x.ToString("X2"))); +} \ No newline at end of file diff --git a/Shared/Logger.cs b/Shared/Logger.cs new file mode 100644 index 0000000..b5c6fa3 --- /dev/null +++ b/Shared/Logger.cs @@ -0,0 +1,23 @@ +namespace Shared; + +public class Logger { + public string Name { get; } + public Logger(string name) { + Name = name; + } + + public void Info(string text) { + Console.ResetColor(); + Console.WriteLine($"Info [{Name}] {text}"); + } + + public void Warn(string text) { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"Warn [{Name}] {text}"); + } + + public void Error(string text) { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"Warn [{Name}] {text}"); + } +} \ No newline at end of file diff --git a/Shared/Packet/PacketAttribute.cs b/Shared/Packet/PacketAttribute.cs new file mode 100644 index 0000000..cbce796 --- /dev/null +++ b/Shared/Packet/PacketAttribute.cs @@ -0,0 +1,9 @@ +namespace Shared.Packet; + +[AttributeUsage(AttributeTargets.Struct)] +public class PacketAttribute : Attribute { + public PacketType Type { get; } + public PacketAttribute(PacketType type) { + Type = type; + } +} \ No newline at end of file diff --git a/Shared/Packet/PacketHeader.cs b/Shared/Packet/PacketHeader.cs new file mode 100644 index 0000000..1783588 --- /dev/null +++ b/Shared/Packet/PacketHeader.cs @@ -0,0 +1,10 @@ +using System.Runtime.InteropServices; + +namespace Shared.Packet; + +[StructLayout(LayoutKind.Sequential)] +public struct PacketHeader { + public Guid Id; + public PacketType Type; + public PacketSender Sender; +} \ No newline at end of file diff --git a/Shared/Packet/PacketSender.cs b/Shared/Packet/PacketSender.cs new file mode 100644 index 0000000..8bea420 --- /dev/null +++ b/Shared/Packet/PacketSender.cs @@ -0,0 +1,6 @@ +namespace Shared.Packet; + +public enum PacketSender { + Server, + Client +} \ No newline at end of file diff --git a/Shared/Packet/PacketType.cs b/Shared/Packet/PacketType.cs new file mode 100644 index 0000000..39b539d --- /dev/null +++ b/Shared/Packet/PacketType.cs @@ -0,0 +1,12 @@ +namespace Shared.Packet; + +public enum PacketType { + Unknown, + Connect, + Player, + Game, + Disconnect, + Costume, + Shine, + Command +} \ No newline at end of file diff --git a/Shared/Packet/Packets/CommandPacket.cs b/Shared/Packet/Packets/CommandPacket.cs new file mode 100644 index 0000000..23a7174 --- /dev/null +++ b/Shared/Packet/Packets/CommandPacket.cs @@ -0,0 +1,5 @@ +namespace Shared.Packet.Packets; + +[Packet(PacketType.Command)] +public struct CommandPacket : IPacket { +} \ No newline at end of file diff --git a/Shared/Packet/Packets/ConnectPacket.cs b/Shared/Packet/Packets/ConnectPacket.cs new file mode 100644 index 0000000..e02d78b --- /dev/null +++ b/Shared/Packet/Packets/ConnectPacket.cs @@ -0,0 +1,6 @@ +namespace Shared.Packet.Packets; + +[Packet(PacketType.Connect)] +public struct ConnectPacket : IPacket { + public ConnectionTypes ConnectionType; +} \ No newline at end of file diff --git a/Shared/Packet/Packets/ConnectionTypes.cs b/Shared/Packet/Packets/ConnectionTypes.cs new file mode 100644 index 0000000..1056318 --- /dev/null +++ b/Shared/Packet/Packets/ConnectionTypes.cs @@ -0,0 +1,6 @@ +namespace Shared.Packet.Packets; + +public enum ConnectionTypes { + FirstConnection, + Reconnecting +} \ No newline at end of file diff --git a/Shared/Packet/Packets/CostumePacket.cs b/Shared/Packet/Packets/CostumePacket.cs new file mode 100644 index 0000000..2080999 --- /dev/null +++ b/Shared/Packet/Packets/CostumePacket.cs @@ -0,0 +1,13 @@ +using System.Runtime.InteropServices; + +namespace Shared.Packet.Packets; + +[Packet(PacketType.Costume)] +public struct CostumePacket : IPacket { + public const int CostumeNameSize = 0x20; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CostumeNameSize)] + public string BodyName; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CostumeNameSize)] + public string CapName; +} \ No newline at end of file diff --git a/Shared/Packet/Packets/DisconnectPacket.cs b/Shared/Packet/Packets/DisconnectPacket.cs new file mode 100644 index 0000000..a266d2d --- /dev/null +++ b/Shared/Packet/Packets/DisconnectPacket.cs @@ -0,0 +1,5 @@ +namespace Shared.Packet.Packets; + +[Packet(PacketType.Disconnect)] +public struct DisconnectPacket : IPacket { +} \ No newline at end of file diff --git a/Shared/Packet/Packets/IPacket.cs b/Shared/Packet/Packets/IPacket.cs new file mode 100644 index 0000000..7e12b64 --- /dev/null +++ b/Shared/Packet/Packets/IPacket.cs @@ -0,0 +1,5 @@ +namespace Shared.Packet.Packets; + +// Packet interface for type safety +public interface IPacket { +} \ No newline at end of file diff --git a/Shared/Packet/Packets/PlayerPacket.cs b/Shared/Packet/Packets/PlayerPacket.cs new file mode 100644 index 0000000..16ac10a --- /dev/null +++ b/Shared/Packet/Packets/PlayerPacket.cs @@ -0,0 +1,25 @@ +using System.Numerics; +using System.Runtime.InteropServices; + +namespace Shared.Packet.Packets; + +[Packet(PacketType.Player)] +public struct PlayerPacket : IPacket { + public const int NameSize = 0x30; + + public Vector3 Position; + public Quaternion Rotation; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)] + public float[] AnimationBlendWeights; + public float AnimationRate; + public bool Flat; + public bool ThrowingCap; + public bool Seeker; + public int ScenarioNum; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)] + public string Stage; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)] + public string Act; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NameSize)] + public string SubAct; +} \ No newline at end of file diff --git a/Shared/Packet/Packets/ShinePacket.cs b/Shared/Packet/Packets/ShinePacket.cs new file mode 100644 index 0000000..9dab7fe --- /dev/null +++ b/Shared/Packet/Packets/ShinePacket.cs @@ -0,0 +1,6 @@ +namespace Shared.Packet.Packets; + +[Packet(PacketType.Shine)] +public struct ShinePacket : IPacket { + public int ShineId; +} \ No newline at end of file diff --git a/Shared/Shared.csproj b/Shared/Shared.csproj new file mode 100644 index 0000000..b718b4d --- /dev/null +++ b/Shared/Shared.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/SmoMultiplayerServer.sln b/SmoMultiplayerServer.sln new file mode 100644 index 0000000..10ffc6b --- /dev/null +++ b/SmoMultiplayerServer.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{AB5DE676-F731-481C-909C-48905B6E5274}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestClient", "TestClient\TestClient.csproj", "{6CA2B314-2D79-46DC-9706-48E003A025BD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Shared", "Shared\Shared.csproj", "{CE222C1F-FBCE-4690-BD14-B7D6290A473E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {AB5DE676-F731-481C-909C-48905B6E5274}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB5DE676-F731-481C-909C-48905B6E5274}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB5DE676-F731-481C-909C-48905B6E5274}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB5DE676-F731-481C-909C-48905B6E5274}.Release|Any CPU.Build.0 = Release|Any CPU + {6CA2B314-2D79-46DC-9706-48E003A025BD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6CA2B314-2D79-46DC-9706-48E003A025BD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6CA2B314-2D79-46DC-9706-48E003A025BD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6CA2B314-2D79-46DC-9706-48E003A025BD}.Release|Any CPU.Build.0 = Release|Any CPU + {CE222C1F-FBCE-4690-BD14-B7D6290A473E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE222C1F-FBCE-4690-BD14-B7D6290A473E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE222C1F-FBCE-4690-BD14-B7D6290A473E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE222C1F-FBCE-4690-BD14-B7D6290A473E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/TestClient/Program.cs b/TestClient/Program.cs new file mode 100644 index 0000000..e9f86e2 --- /dev/null +++ b/TestClient/Program.cs @@ -0,0 +1,16 @@ +using System.Net.Sockets; +using System.Runtime.InteropServices; +using Shared; +using Shared.Packet; +using Shared.Packet.Packets; + +TcpClient client = new TcpClient("127.0.0.1", 1027); +Guid ownId = new Guid(); +Logger logger = new Logger("Client"); +NetworkStream stream = client.GetStream(); + + +// void WritePacket(Span data, IPacket packet) { +// MemoryMarshal.Write(); +// } +// stream.Write(); \ No newline at end of file diff --git a/TestClient/TestClient.csproj b/TestClient/TestClient.csproj new file mode 100644 index 0000000..7d518c2 --- /dev/null +++ b/TestClient/TestClient.csproj @@ -0,0 +1,14 @@ + + + + Exe + net6.0 + enable + enable + + + + + + +