commit 71269033a38b0db882facef972b84fd1f733ecb6 Author: Benjo Date: Fri Feb 19 16:22:17 2021 +0100 First commit with everything diff --git a/.idea/.idea.GameServer/.idea/.gitignore b/.idea/.idea.GameServer/.idea/.gitignore new file mode 100644 index 0000000..6295c48 --- /dev/null +++ b/.idea/.idea.GameServer/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/.idea.GameServer.iml +/modules.xml +/projectSettingsUpdater.xml +# Datasource local storage ignored files +/../../../../../../../../../:\Dateien\Documents\Programming\C#\GameServer\.idea\.idea.GameServer\.idea/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/.idea.GameServer/.idea/encodings.xml b/.idea/.idea.GameServer/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.GameServer/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.GameServer/.idea/indexLayout.xml b/.idea/.idea.GameServer/.idea/indexLayout.xml new file mode 100644 index 0000000..27ba142 --- /dev/null +++ b/.idea/.idea.GameServer/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.GameServer/.idea/riderModule.iml b/.idea/.idea.GameServer/.idea/riderModule.iml new file mode 100644 index 0000000..1a4e0d9 --- /dev/null +++ b/.idea/.idea.GameServer/.idea/riderModule.iml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.GameServer/.idea/vcs.xml b/.idea/.idea.GameServer/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.GameServer/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Arch/DatePrefix.cs b/Arch/DatePrefix.cs new file mode 100644 index 0000000..a45fc40 --- /dev/null +++ b/Arch/DatePrefix.cs @@ -0,0 +1,21 @@ +using System; +using System.IO; +using System.Text; + +namespace GameServer.Arch { + + public class DatePrefix : TextWriter { + + private readonly TextWriter _originalOut; + + public DatePrefix() { + _originalOut = Console.Out; + } + + public override Encoding Encoding => new ASCIIEncoding(); + + public override void WriteLine(string value) { + _originalOut.WriteLine($"{DateTime.Now} {value}"); + } + } +} diff --git a/Arch/Packet.cs b/Arch/Packet.cs new file mode 100644 index 0000000..6544e0c --- /dev/null +++ b/Arch/Packet.cs @@ -0,0 +1,338 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using System.Text; +using GameServer.Management; + +namespace GameServer.Arch { + public sealed class Packet : IDisposable { + private List _buffer; + + private bool _disposed; + private byte[] _readableBuffer; + private int _readPos; + + /// Creates a new empty packet (without an ID). + public Packet() { + _buffer = new List(); // Initialize buffer + _readPos = 0; // Set readPos to 0 + } + + /// Creates a new packet with a given ID. Used for sending. + /// The packetType ID. + /// The packetAction ID. + public Packet(int packetTypeId, int packetActionId) { + _buffer = new List(); // Initialize buffer + _readPos = 0; // Set readPos to 0 + + Write(packetTypeId); // Write packet ids to the buffer + Write(packetActionId); + } + + /// Creates a packet from which data can be read. Used for receiving. + /// The bytes to add to the packet. + public Packet(byte[] data) { + _buffer = new List(); // Initialize buffer + _readPos = 0; // Set readPos to 0 + + SetBytes(data); + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) { + if (_disposed) + return; + + if (disposing) { + _buffer = null; + _readableBuffer = null; + _readPos = 0; + } + + _disposed = true; + } + + #region Functions + /// Sets the packet's content and prepares it to be read. + /// The bytes to add to the packet. + public void SetBytes(byte[] data) { + Write(data); + _readableBuffer = _buffer.ToArray(); + } + + /// Inserts the length of the packet's content at the start of the buffer. + public void WriteLength() { + _buffer.InsertRange(0, BitConverter.GetBytes(_buffer.Count)); // Insert the byte length of the packet at the very beginning + } + + /// Inserts the given int at the start of the buffer. + /// The int to insert. + public void InsertInt(int value) { + _buffer.InsertRange(0, BitConverter.GetBytes(value)); // Insert the int at the start of the buffer + } + + /// Gets the packet's content in array form. + public byte[] ToArray() { + _readableBuffer = _buffer.ToArray(); + return _readableBuffer; + } + + /// Gets the length of the packet's content. + public int Length() { + return _buffer.Count; // Return the length of buffer + } + + /// Gets the length of the unread data contained in the packet. + public int UnreadLength() { + return Length() - _readPos; // Return the remaining length (unread) + } + + /// Resets the packet instance to allow it to be reused. + /// Whether or not to reset the packet. + public void Reset(bool shouldReset = true) { + if (shouldReset) { + _buffer.Clear(); // Clear buffer + _readableBuffer = null; + _readPos = 0; // Reset readPos + } else { + _readPos -= 4; // "Unread" the last read int + } + } + #endregion + + #region Write Data + /// Adds a byte to the packet. + /// The byte to add. + public void Write(byte value) { + _buffer.Add(value); + } + + /// Adds an array of bytes to the packet. + /// The byte array to add. + public void Write(IEnumerable value) { + _buffer.AddRange(value); + } + + /// Adds a short to the packet. + /// The short to add. + public void Write(short value) { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + + /// Adds an int to the packet. + /// The int to add. + public void Write(int value) { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + + /// Adds a long to the packet. + /// The long to add. + public void Write(long value) { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + + /// Adds a float to the packet. + /// The float to add. + public void Write(float value) { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + + /// Adds a bool to the packet. + /// The bool to add. + public void Write(bool value) { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + + /// Adds a string to the packet. + /// The string to add. + public void Write(string value) { + Write(value.Length); // Add the length of the string to the packet + _buffer.AddRange(Encoding.ASCII.GetBytes(value)); // Add the string itself + } + + /// Adds a Vector3 to the packet. + /// The Vector3 to add. + public void Write(Vector3 value) { + Write(value.X); + Write(value.Y); + Write(value.Z); + } + + /// Adds a Quaternion to the packet. + /// The Quaternion to add. + public void Write(Quaternion value) { + Write(value.X); + Write(value.Y); + Write(value.Z); + Write(value.W); + } + + /// Adds a Room to the packet. + /// The Room to add. + public void Write(Room value) { + Write(value.Id); + Write(value.Name); + Write(value.IsLocked); + Write(value.Leader.Id); + Write(value.MaxPlayers); + Write(value.CurrentPlayers); + foreach (var client in value.Clients) { + Write(client.Id); + Write(client.Name); + } + } + + /// Adds ClientProperties to the packet. + /// ClientProperties to add. + public void Write(Dictionary value) { + Write(value.Count); + foreach (var (key, clientProperties) in value) { + Write(key); + Write(clientProperties.IsLeader); + Write(clientProperties.IsReady); + Write(clientProperties.ColorId); + } + } + + #endregion + + #region Read Data + /// Reads a byte from the packet. + /// Whether or not to move the buffer's read position. + public byte ReadByte(bool moveReadPos = true) { + if (_buffer.Count > _readPos) { + // If there are unread bytes + var value = _readableBuffer[_readPos]; // Get the byte at readPos' position + if (moveReadPos) + // If _moveReadPos is true + _readPos += 1; // Increase readPos by 1 + return value; // Return the byte + } + + throw new Exception("Could not read value of type 'byte'!"); + } + + /// Reads an array of bytes from the packet. + /// The length of the byte array. + /// Whether or not to move the buffer's read position. + public byte[] ReadBytes(int length, bool moveReadPos = true) { + if (_buffer.Count > _readPos) { + // If there are unread bytes + var value = _buffer.GetRange(_readPos, length).ToArray(); // Get the bytes at readPos' position with a range of _length + if (moveReadPos) + // If _moveReadPos is true + _readPos += length; // Increase readPos by _length + return value; // Return the bytes + } + + throw new Exception("Could not read value of type 'byte[]'!"); + } + + /// Reads a short from the packet. + /// Whether or not to move the buffer's read position. + public short ReadShort(bool moveReadPos = true) { + if (_buffer.Count > _readPos) { + // If there are unread bytes + var value = BitConverter.ToInt16(_readableBuffer, _readPos); // Convert the bytes to a short + if (moveReadPos) + // If _moveReadPos is true and there are unread bytes + _readPos += 2; // Increase readPos by 2 + return value; // Return the short + } + + throw new Exception("Could not read value of type 'short'!"); + } + + /// Reads an int from the packet. + /// Whether or not to move the buffer's read position. + public int ReadInt(bool moveReadPos = true) { + if (_buffer.Count > _readPos) { + // If there are unread bytes + var value = BitConverter.ToInt32(_readableBuffer, _readPos); // Convert the bytes to an int + if (moveReadPos) + // If _moveReadPos is true + _readPos += 4; // Increase readPos by 4 + return value; // Return the int + } + + throw new Exception("Could not read value of type 'int'!"); + } + + /// Reads a long from the packet. + /// Whether or not to move the buffer's read position. + public long ReadLong(bool moveReadPos = true) { + if (_buffer.Count > _readPos) { + // If there are unread bytes + var value = BitConverter.ToInt64(_readableBuffer, _readPos); // Convert the bytes to a long + if (moveReadPos) + // If _moveReadPos is true + _readPos += 8; // Increase readPos by 8 + return value; // Return the long + } + + throw new Exception("Could not read value of type 'long'!"); + } + + /// Reads a float from the packet. + /// Whether or not to move the buffer's read position. + public float ReadFloat(bool moveReadPos = true) { + if (_buffer.Count > _readPos) { + // If there are unread bytes + var value = BitConverter.ToSingle(_readableBuffer, _readPos); // Convert the bytes to a float + if (moveReadPos) + // If _moveReadPos is true + _readPos += 4; // Increase readPos by 4 + return value; // Return the float + } + + throw new Exception("Could not read value of type 'float'!"); + } + + /// Reads a bool from the packet. + /// Whether or not to move the buffer's read position. + public bool ReadBool(bool moveReadPos = true) { + if (_buffer.Count > _readPos) { + // If there are unread bytes + var value = BitConverter.ToBoolean(_readableBuffer, _readPos); // Convert the bytes to a bool + if (moveReadPos) + // If _moveReadPos is true + _readPos += 1; // Increase readPos by 1 + return value; // Return the bool + } + + throw new Exception("Could not read value of type 'bool'!"); + } + + /// Reads a string from the packet. + /// Whether or not to move the buffer's read position. + public string ReadString(bool moveReadPos = true) { + try { + var length = ReadInt(); // Get the length of the string + var value = Encoding.ASCII.GetString(_readableBuffer, _readPos, length); // Convert the bytes to a string + if (moveReadPos && value.Length > 0) + // If _moveReadPos is true string is not empty + _readPos += length; // Increase readPos by the length of the string + return value; // Return the string + } catch { + throw new Exception("Could not read value of type 'string'!"); + } + } + + /// Reads a Vector3 from the packet. + /// Whether or not to move the buffer's read position. + public Vector3 ReadVector3(bool moveReadPos = true) { + return new(ReadFloat(moveReadPos), ReadFloat(moveReadPos), ReadFloat(moveReadPos)); + } + + /// Reads a Quaternion from the packet. + /// Whether or not to move the buffer's read position. + public Quaternion ReadQuaternion(bool moveReadPos = true) { + return new(ReadFloat(moveReadPos), ReadFloat(moveReadPos), ReadFloat(moveReadPos), ReadFloat(moveReadPos)); + } + #endregion + } +} diff --git a/Arch/ProtocolManager.cs b/Arch/ProtocolManager.cs new file mode 100644 index 0000000..8a0922a --- /dev/null +++ b/Arch/ProtocolManager.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; +using GameServer.Management; + +namespace GameServer.Arch { + + public static class Listener { + private static TcpListener _tcpListener; + private static UdpClient _udpListener; + + private static Dictionary Clients => Server.Clients; + + public static void Start() { + _tcpListener = new TcpListener(IPAddress.Any, Server.Port); + _tcpListener.Start(); + _tcpListener.BeginAcceptTcpClient(TcpConnectCallback, null); + + _udpListener = new UdpClient(Server.Port); + _udpListener.BeginReceive(UdpReceiveCallback, null); + } + + private static void TcpConnectCallback(IAsyncResult result) { + var client = _tcpListener.EndAcceptTcpClient(result); + _tcpListener.BeginAcceptTcpClient(TcpConnectCallback, null); + Console.WriteLine($"Incoming connection from {client.Client.RemoteEndPoint}..."); + + for (var i = 1; i <= Server.MaxPlayers; i++) { + if (Clients[i].Tcp.Socket != null) + continue; + + Clients[i].Tcp.Connect(client); + return; + } + + Console.WriteLine($"{client.Client.RemoteEndPoint} failed to connect: Server full!"); + } + + private static void UdpReceiveCallback(IAsyncResult result) { + try { + var clientEndPoint = new IPEndPoint(IPAddress.Any, 0); + var data = _udpListener.EndReceive(result, ref clientEndPoint); + _udpListener.BeginReceive(UdpReceiveCallback, null); + + if (data.Length < 4) + return; + + using var packet = new Packet(data); + var clientId = packet.ReadInt(); + + if (clientId == 0) + return; + + if (Clients[clientId].Tcp.Socket == null) + return; + + if (Clients[clientId].Udp.EndPoint == null) { + Clients[clientId].Udp.Connect(clientEndPoint); + return; + } + + if (Clients[clientId].Udp.EndPoint.ToString() == clientEndPoint.ToString()) + Clients[clientId].Udp.HandleData(packet); + + } catch (Exception ex) { + Console.WriteLine($"Error receiving UDP data: {ex}"); + } + } + + public static void SendUdpData(IPEndPoint clientEndPoint, Packet packet) { + try { + if (clientEndPoint != null) + _udpListener.BeginSend(packet.ToArray(), packet.Length(), clientEndPoint, null, null); + } catch (Exception ex) { + Console.WriteLine($"Error sending data to {clientEndPoint} via UDP: {ex}"); + } + } + } + + public class TcpManager { + private readonly int _id; + private byte[] _receiveBuffer; + private Packet _receivedData; + private NetworkStream _stream; + public TcpClient Socket; + + public TcpManager(int id) { + _id = id; + } + + public void Connect(TcpClient socket) { + Socket = socket; + Socket.ReceiveBufferSize = Constants.DataBufferSize; + Socket.SendBufferSize = Constants.DataBufferSize; + + _stream = Socket.GetStream(); + + _receivedData = new Packet(); + _receiveBuffer = new byte[Constants.DataBufferSize]; + + _stream.BeginRead(_receiveBuffer, 0, Constants.DataBufferSize, ReceiveCallback, null); + + Server.Clients[_id].OnConnect(); + } + + public void SendData(Packet packet) { + try { + if (Socket != null) + _stream.BeginWrite(packet.ToArray(), 0, packet.Length(), null, null); + } catch (Exception ex) { + Console.WriteLine($"Error sending data to player {_id} via TCP: {ex}"); + } + } + + private void ReceiveCallback(IAsyncResult result) { + try { + var byteLength = _stream.EndRead(result); + if (byteLength <= 0) { + Server.Clients[_id].Disconnect(); + return; + } + + var data = new byte[byteLength]; + Array.Copy(_receiveBuffer, data, byteLength); + + _receivedData.Reset(HandleData(data)); + _stream.BeginRead(_receiveBuffer, 0, Constants.DataBufferSize, ReceiveCallback, null); + } catch (Exception ex) { + Console.WriteLine($"Error receiving TCP data: {ex}"); + Server.Clients[_id].Disconnect(); + } + } + + private bool HandleData(byte[] data) { + var packetLength = 0; + + _receivedData.SetBytes(data); + + if (_receivedData.UnreadLength() >= 4) { + packetLength = _receivedData.ReadInt(); + if (packetLength <= 0) + return true; + } + + while (packetLength > 0 && packetLength <= _receivedData.UnreadLength()) { + var packetBytes = _receivedData.ReadBytes(packetLength); + ThreadManager.ExecuteOnMainThread(() => ReadPacket.Read(packetBytes, _id)); + + packetLength = 0; + if (_receivedData.UnreadLength() < 4) + continue; + + packetLength = _receivedData.ReadInt(); + if (packetLength <= 0) + return true; + } + + return packetLength <= 1; + + } + + public void Disconnect() { + Socket.Close(); + _stream = null; + _receivedData = null; + _receiveBuffer = null; + Socket = null; + } + } + + public class UdpManager { + + private readonly int _id; + public IPEndPoint EndPoint; + + public UdpManager(int id) { + _id = id; + } + + public void Connect(IPEndPoint endPoint) { + EndPoint = endPoint; + } + + public void SendData(Packet packet) { + Listener.SendUdpData(EndPoint, packet); + } + + public void HandleData(Packet packetData) { + var packetLength = packetData.ReadInt(); + var packetBytes = packetData.ReadBytes(packetLength); + + ThreadManager.ExecuteOnMainThread(() => ReadPacket.Read(packetBytes, _id)); + } + + public void Disconnect() { + EndPoint = null; + } + } + + internal static class ReadPacket { + public static void Read(byte[] packetBytes, int clientId) { + using var packet = new Packet(packetBytes); + + var packetTypeId = packet.ReadInt(); + var packetActionId = packet.ReadInt(); + Server.PacketHandlers[packetTypeId][packetActionId](clientId, packet); + } + } +} diff --git a/Arch/SendData.cs b/Arch/SendData.cs new file mode 100644 index 0000000..8bc67d7 --- /dev/null +++ b/Arch/SendData.cs @@ -0,0 +1,81 @@ +using System; +using System.Linq; +using GameServer.Management; + +namespace GameServer.Arch { + public static class SendData { + public static void SendTcpData(int toClient, Packet packet) { + packet.WriteLength(); + Server.Clients[toClient].Tcp.SendData(packet); + } + + public static void SendTcpDataToRoom(Room room, Packet packet) { + packet.WriteLength(); + foreach (var client in room.Clients) { + client.Tcp.SendData(packet); + } + } + + public static void SendTcpDataToRoom(Room room, Packet packet, int withClient) { + SendTcpDataToRoom(room, packet); + Server.Clients[withClient].Tcp.SendData(packet); + } + + public static void SendTcpDataToRoom(Room room, int exceptClient, Packet packet) { + packet.WriteLength(); + foreach (var client in room.Clients.Where(client => client.Id != exceptClient)) { + client.Tcp.SendData(packet); + } + } + + public static void SendTcpDataToAll(Packet packet, Func condition) { + packet.WriteLength(); + foreach (var client in Server.Clients.Values.Where(condition)) { + client.Tcp.SendData(packet); + } + } + + public static void SendTcpDataToAll(Packet packet) { + SendTcpDataToAll(packet, _ => true); + } + + public static void SendTcpDataToAll(int exceptClient, Packet packet) { + packet.WriteLength(); + for (var i = 1; i <= Server.MaxPlayers; i++) + if (i != exceptClient) + Server.Clients[i].Tcp.SendData(packet); + } + + public static void SendUdpData(int toClient, Packet packet) { + packet.WriteLength(); + Server.Clients[toClient].Udp.SendData(packet); + } + + public static void SendUdpDataToAll(Room room, Packet packet) { + packet.WriteLength(); + foreach (var client in room.Clients) { + client.Udp.SendData(packet); + } + } + + public static void SendUdpDataToAll(Room room, int exceptClient, Packet packet) { + packet.WriteLength(); + foreach (var client in room.Clients.Where(client => client.Id != exceptClient)) { + client.Udp.SendData(packet); + } + } + + public static void SendUdpDataToAll(Packet packet) { + packet.WriteLength(); + for (var i = 1; i <= Server.MaxPlayers; i++) + Server.Clients[i].Udp.SendData(packet); + } + + public static void SendUdpDataToAll(int exceptClient, Packet packet) { + packet.WriteLength(); + for (var i = 1; i <= Server.MaxPlayers; i++) + if (i != exceptClient) + Server.Clients[i].Udp.SendData(packet); + } + } +} diff --git a/Arch/ThreadManager.cs b/Arch/ThreadManager.cs new file mode 100644 index 0000000..7eb02b6 --- /dev/null +++ b/Arch/ThreadManager.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using GameServer.Management; + +namespace GameServer.Arch { + + internal static class ThreadManager { + private static readonly List ToExecuteOnMainThread = new(); + private static readonly List ExecuteCopiedOnMainThread = new(); + private static bool _actionToExecuteOnMainThread; + + /// Sets an action to be executed on the main thread. + /// The action to be executed on the main thread. + public static void ExecuteOnMainThread(Action action) { + if (action == null) { + Console.WriteLine("No action to execute on main thread!"); + return; + } + + lock (ToExecuteOnMainThread) { + ToExecuteOnMainThread.Add(action); + _actionToExecuteOnMainThread = true; + } + } + + /// Executes all code meant to run on the main thread. NOTE: Call this ONLY from the main thread. + private static void UpdateMain() { + if (!_actionToExecuteOnMainThread) + return; + + ExecuteCopiedOnMainThread.Clear(); + lock (ToExecuteOnMainThread) { + ExecuteCopiedOnMainThread.AddRange(ToExecuteOnMainThread); + ToExecuteOnMainThread.Clear(); + _actionToExecuteOnMainThread = false; + } + + foreach (var t in ExecuteCopiedOnMainThread) + t(); + } + + public static bool IsRunning; + + public static void MainThread() { + Console.WriteLine($"Main thread started. Running at {Constants.TicksPerSec} ticks per second."); + var nextLoop = DateTime.Now; + + while (IsRunning) + while (nextLoop < DateTime.Now) { + Tick(); + + nextLoop = nextLoop.AddMilliseconds(Constants.MsPerTick); + + if (nextLoop > DateTime.Now) + Thread.Sleep(nextLoop - DateTime.Now); + } + } + + private static void Tick() { + foreach (var room in Server.Rooms.Values.Where(room => room.Game != null && room.Game.IsRunning)) { + room.Game.Update(); + } + + UpdateMain(); + } + } +} diff --git a/Game/GameHandle.cs b/Game/GameHandle.cs new file mode 100644 index 0000000..24234ff --- /dev/null +++ b/Game/GameHandle.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using GameServer.Arch; +using GameServer.Management; +using GameServer.PacketTypes; + +namespace GameServer.Game { + public static class GameHandle { + + public static void Action(int fromClient, Packet packet) { + + } + } +} diff --git a/Game/GameManager.cs b/Game/GameManager.cs new file mode 100644 index 0000000..6c25000 --- /dev/null +++ b/Game/GameManager.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using GameServer.Management; + +namespace GameServer.Game { + public class GameManager { + + public bool IsRunning { get; set; } + + private Room Room { get; } + private Dictionary Players { get; } = new(); + + public GameManager(Room room) { + Room = room; + foreach (var client in room.Clients) { + Players.Add(client.Id, new Player(client.Id, client.Name)); + } + } + + public void Start() { + foreach (var player in Players.Values) { + player.Start(); + } + } + + public void Update() { + foreach (var player in Players.Values) { + player.Update(); + } + } + + } +} diff --git a/Game/GameSend.cs b/Game/GameSend.cs new file mode 100644 index 0000000..5dbd577 --- /dev/null +++ b/Game/GameSend.cs @@ -0,0 +1,14 @@ +using GameServer.Arch; +using GameServer.Management; +using GameServer.PacketTypes; +using static GameServer.Arch.SendData; + +namespace GameServer.Game { + public static class GameSend { + + private static Packet CreatePacket(ServerGamePacket type) { + return new((int)PacketType.Game, (int)type); + } + + } +} diff --git a/Game/Player.cs b/Game/Player.cs new file mode 100644 index 0000000..c6ee82b --- /dev/null +++ b/Game/Player.cs @@ -0,0 +1,20 @@ +namespace GameServer.Game { + public class Player { + + public int Id { get; } + public string Name { get; } + + public Player(int id, string name) { + Id = id; + Name = name; + } + + public void Start() { + + } + + public void Update() { + + } + } +} diff --git a/GameServer.csproj b/GameServer.csproj new file mode 100644 index 0000000..9590466 --- /dev/null +++ b/GameServer.csproj @@ -0,0 +1,8 @@ + + + + Exe + net5.0 + + + diff --git a/GameServer.sln b/GameServer.sln new file mode 100644 index 0000000..780ced9 --- /dev/null +++ b/GameServer.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GameServer", "GameServer.csproj", "{A3BE7051-E90A-48ED-93E9-F7C46B05390B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A3BE7051-E90A-48ED-93E9-F7C46B05390B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A3BE7051-E90A-48ED-93E9-F7C46B05390B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A3BE7051-E90A-48ED-93E9-F7C46B05390B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A3BE7051-E90A-48ED-93E9-F7C46B05390B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Management/Client.cs b/Management/Client.cs new file mode 100644 index 0000000..0a2cccc --- /dev/null +++ b/Management/Client.cs @@ -0,0 +1,45 @@ +using System; +using GameServer.Arch; +using GameServer.Game; + +namespace GameServer.Management { + public class Client { + + public readonly int Id; + public readonly TcpManager Tcp; + public readonly UdpManager Udp; + + public string Name { get; set; } + public Player Player; + public Room Room { get; set; } + + public Client(int clientId) { + Id = clientId; + Tcp = new TcpManager(Id); + Udp = new UdpManager(Id); + } + + public void OnConnect() { + ServerSend.Welcome(Id, "Welcome to the server!"); + if (Tcp.Socket.Client.RemoteEndPoint != null) + _endpoint = Tcp.Socket.Client.RemoteEndPoint.ToString(); + } + + public void Disconnect() { + Tcp.Disconnect(); + Udp.Disconnect(); + + if (Room != null) + Server.LeaveRoom(this); + + Player = null; + + Console.WriteLine($"{this} has disconnected."); + } + + private string _endpoint = "Client"; + public override string ToString() { + return $"{{\"{Name}\" | {_endpoint}}}"; + } + } +} diff --git a/Management/Constants.cs b/Management/Constants.cs new file mode 100644 index 0000000..4e6132e --- /dev/null +++ b/Management/Constants.cs @@ -0,0 +1,8 @@ +namespace GameServer.Management { + public static class Constants { + public const int TicksPerSec = 32; + public const int MsPerTick = 1000 / TicksPerSec; + public const int DataBufferSize = 4096; + public const int CountdownSeconds = 2; + } +} diff --git a/Management/Room.cs b/Management/Room.cs new file mode 100644 index 0000000..e1e0ea7 --- /dev/null +++ b/Management/Room.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameServer.Game; + +namespace GameServer.Management { + public class Room { + + public class ClientProperties { + public bool IsLeader; + public bool IsReady; + public int ColorId; + } + + public string Id { get; } + public string Name { get; } + public string Password { get; } + public int MaxPlayers { get; } + public int CurrentPlayers => Clients.Count; + public bool IsFull => CurrentPlayers == MaxPlayers; + + public bool IsLocked { get; set; } + + public GameManager Game { get; private set; } + + public void StartGame() { + Game = new GameManager(this); + Game.Start(); + } + + public readonly Dictionary ClientPropertiesMap = new(); + + public Client Leader { + get { + return CurrentPlayers == 0 ? null + : + Server.Clients[ClientPropertiesMap.Single(pair => pair.Value.IsLeader).Key]; + } + set { + foreach (var clientId in ClientPropertiesMap.Keys) { + ClientPropertiesMap[clientId].IsLeader = false; + } + ClientPropertiesMap[value.Id].IsLeader = true; + } + } + + private readonly List _clientIds = new(); + public List Clients { + get { + var list = new List(); + foreach (int clientId in _clientIds) { + list.Add(Server.Clients[clientId]); + } + return list; + } + } + + public void SetReady(Client client, bool isReady) { + ClientPropertiesMap[client.Id].IsReady = isReady; + } + + public void SetColor(Client client, int colorId) { + if (ClientPropertiesMap.Values.Any(properties => properties.ColorId.Equals(colorId))) + return; + + ClientPropertiesMap[client.Id].ColorId = colorId; + } + + public Room(string id, Client leader, string name, string password, int maxPlayers) { + Id = id; + AddClient(leader); + Leader = leader; + Name = name; + Password = password; + MaxPlayers = maxPlayers; + IsLocked = false; + } + + public void AddClient(Client client) { + _clientIds.Add(client.Id); + client.Room = this; + for (int i = 0; i < 10; i++) { + if (ClientPropertiesMap.Values.Any(prop => prop.ColorId.Equals(i))) + continue; + + ClientPropertiesMap.Add(client.Id, new ClientProperties { + IsReady = false, + ColorId = i, + IsLeader = false + }); + break; + } + } + + public bool RemoveClient(Client leftClient) { + var leader = Leader; + + _clientIds.Remove(leftClient.Id); + leftClient.Room = null; + ClientPropertiesMap.Remove(leftClient.Id); + + if (CurrentPlayers == 0) { + return false; + } + + if (!leftClient.Equals(leader)) + return true; + + Leader = Clients.First(); + Console.WriteLine($"{Leader} is the new leader of room {this}"); + + return true; + } + + public void KickClient(Client kickClient) { + _clientIds.Remove(kickClient.Id); + kickClient.Room = null; + ClientPropertiesMap.Remove(kickClient.Id); + } + + public override string ToString() { + return $"{{\"{Name}\" | \"{Id.Substring(0, 10)}...\" | ({CurrentPlayers}/{MaxPlayers})}}"; + } + } +} diff --git a/Management/RoomHandle.cs b/Management/RoomHandle.cs new file mode 100644 index 0000000..53753f3 --- /dev/null +++ b/Management/RoomHandle.cs @@ -0,0 +1,121 @@ +using System; +using GameServer.Arch; + +namespace GameServer.Management { + public static class RoomHandle { + public static void RoomList(int fromClientId, Packet packet) { + RoomSend.List(fromClientId); + + Client client = Server.Clients[fromClientId]; + + Console.WriteLine($"{client} requested a list of rooms."); + } + + public static void RoomCreate(int fromClientId, Packet packet) { + string roomName = packet.ReadString(); + string roomPassword = packet.ReadString(); + int maxPlayers = packet.ReadInt(); + + Server.CreateRoom(fromClientId, roomName, roomPassword, maxPlayers); + } + + public static void RoomJoin(int fromClientId, Packet packet) { + string roomId = packet.ReadString(); + string password = packet.ReadString(); + + Client client = Server.Clients[fromClientId]; + Room room = Server.Rooms[roomId]; + + if (room == null) + return; + + if (room.IsLocked) + return; + + Server.JoinRoom(client, room, password); + } + + public static void RoomLeave(int fromClientId, Packet packet) { + Client client = Server.Clients[fromClientId]; + + if (client.Room == null) + return; + + if (client.Room.IsLocked) + return; + + Server.LeaveRoom(client); + } + + public static void RoomKick(int fromClientId, Packet packet) { + int kickId = packet.ReadInt(); + + Client leaderClient = Server.Clients[fromClientId]; + + if (leaderClient.Room == null) + return; + + Client kickClient = Server.Clients[kickId]; + + if (kickClient.Room == null) + return; + + if (!kickClient.Room.Equals(leaderClient.Room)) + return; + + Server.KickFromRoom(leaderClient, kickClient); + } + + public static void RoomLeader(int fromClientId, Packet packet) { + var nextLeaderId = packet.ReadInt(); + var nextLeader = Server.Clients[nextLeaderId]; + var fromClient = Server.Clients[fromClientId]; + if (fromClient.Room == null || nextLeader.Room == null || fromClient.Room != nextLeader.Room) + return; + + if (!fromClient.Room.Leader.Id.Equals(fromClientId)) + return; + + fromClient.Room.Leader = nextLeader; + + RoomSend.Properties(fromClient.Room); + } + + public static void RoomReady(int fromClientId, Packet packet) { + Client client = Server.Clients[fromClientId]; + + if (client.Room == null) + return; + + bool isReady = packet.ReadBool(); + client.Room.SetReady(client, isReady); + + RoomSend.Properties(client.Room); + } + + public static void RoomColor(int fromClientId, Packet packet) { + var fromClient = Server.Clients[fromClientId]; + + var colorId = packet.ReadInt(); + + if (fromClient.Room == null) + return; + + fromClient.Room.SetColor(fromClient, colorId); + + RoomSend.Properties(fromClient.Room); + } + + public static void RoomStart(int fromClientId, Packet packet) { + var fromClient = Server.Clients[fromClientId]; + + if (fromClient.Room == null) + return; + + if (!fromClient.Room.Leader.Id.Equals(fromClientId)) + return; + + Server.StartRoom(fromClient.Room); + } + } +} diff --git a/Management/RoomSend.cs b/Management/RoomSend.cs new file mode 100644 index 0000000..c19a64c --- /dev/null +++ b/Management/RoomSend.cs @@ -0,0 +1,106 @@ +using System; + +using GameServer.Arch; +using GameServer.PacketTypes; +using static GameServer.Arch.SendData; +using static GameServer.PacketTypes.ServerRoomPacket; + +namespace GameServer.Management { + public static class RoomSend { + private static Packet CreatePacket(ServerRoomPacket type) { + return new((int)PacketType.Room, (int)type); + } + + public static void List(int toClient) { + using var packet = CreatePacket(RList); + + packet.Write(Server.Rooms.Count); + foreach (var room in Server.Rooms.Values) + packet.Write(room); + + SendTcpData(toClient, packet); + } + + public static void ListUpdate() { + using var packet = CreatePacket(RList); + + packet.Write(Server.Rooms.Count); + foreach (var room in Server.Rooms.Values) + packet.Write(room); + + SendTcpDataToAll(packet, client => client.Room == null); + } + + public static void Created(int toClient, Room room) { + using var packet = CreatePacket(RCreated); + + packet.Write(room); + packet.Write(room.ClientPropertiesMap); + + SendTcpData(toClient, packet); + } + + public static void Joined(int joinedClient, Room room) { + using var packet = CreatePacket(RJoined); + + packet.Write(room); + packet.Write(joinedClient); + packet.Write(room.ClientPropertiesMap); + + SendTcpDataToRoom(room, packet); + } + + public static void Left(int leftClient, Room room) { + using var packet = CreatePacket(RLeft); + + packet.Write(leftClient); + packet.Write(room.ClientPropertiesMap); + + SendTcpDataToRoom(room, packet, leftClient); + } + + public static void CreateFailed(int toClient, string message) { + using var packet = CreatePacket(RCreateFailed); + + packet.Write(message); + + SendTcpData(toClient, packet); + } + + public static void JoinFailed(int toClient, Room room, string message) { + using var packet = CreatePacket(RJoinFailed); + + packet.Write(room.Id); + packet.Write(message); + + SendTcpData(toClient, packet); + } + + public static void KickFailed(int toClient, string message) { + using var packet = CreatePacket(RKickFailed); + + packet.Write(message); + + SendTcpData(toClient, packet); + } + + public static void Properties(Room room) { + using var packet = CreatePacket(RProperties); + + packet.Write(room.ClientPropertiesMap); + + SendTcpDataToRoom(room, packet); + } + + public static void Start(Room room) { + using var packet = CreatePacket(RStart); + + DateTime startTime = DateTime.Now.AddSeconds(Constants.CountdownSeconds); + packet.Write(startTime.Ticks); + packet.Write(room); + packet.Write(room.ClientPropertiesMap); + + SendTcpDataToRoom(room, packet); + } + } +} diff --git a/Management/Server.cs b/Management/Server.cs new file mode 100644 index 0000000..c9044f0 --- /dev/null +++ b/Management/Server.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using GameServer.Arch; +using GameServer.Game; +using GameServer.PacketTypes; + +namespace GameServer.Management { + + internal static class Server { + public delegate void PacketHandler(int fromClient, Packet packet); + public static readonly Dictionary Clients = new(); + public static readonly Dictionary Rooms = new(); + public static Dictionary> PacketHandlers; + + public static int MaxPlayers { get; private set; } + + public static int Port { get; private set; } + + public static void Start(int maxPlayers, int port){ + MaxPlayers = maxPlayers; + Port = port; + + Console.WriteLine("Starting server..."); + InitializeServerData(); + + Listener.Start(); + + Console.WriteLine($"Server started on port {Port}."); + } + + private static void InitializeServerData(){ + for (var i = 1; i <= MaxPlayers; i++) { + Clients.Add(i, new Client(i)); + } + + PacketHandlers = new Dictionary> { + {(int)PacketType.Default, new Dictionary { + {(int)ClientDefaultPacket.DWelcomeReceived, ServerHandle.WelcomeReceived}, + }}, + {(int)PacketType.Room, new Dictionary { + {(int)ClientRoomPacket.RList, RoomHandle.RoomList}, + {(int)ClientRoomPacket.RCreate, RoomHandle.RoomCreate}, + {(int)ClientRoomPacket.RJoin, RoomHandle.RoomJoin}, + {(int)ClientRoomPacket.RLeave, RoomHandle.RoomLeave}, + {(int)ClientRoomPacket.RKick, RoomHandle.RoomKick}, + {(int)ClientRoomPacket.RLeader, RoomHandle.RoomLeader}, + {(int)ClientRoomPacket.RReady, RoomHandle.RoomReady}, + {(int)ClientRoomPacket.RColor, RoomHandle.RoomColor}, + {(int)ClientRoomPacket.RStart, RoomHandle.RoomStart}, + }}, + {(int)PacketType.Game, new Dictionary { + {(int)ClientGamePacket.Action, GameHandle.Action} + }} + }; + + Console.WriteLine("Initialized packets."); + } + + public static void CreateRoom(int leaderId, string name, string password, int maxPlayers) { + var leader = Clients[leaderId]; + + if (leader.Room != null) { + RoomSend.CreateFailed(leaderId, "Failed to create room!"); + Console.WriteLine($"{leader} tried to create a room while already being in one!"); + return; + } + + string id; + do { + id = Guid.NewGuid().ToString(); + } while (Rooms.ContainsKey(id)); + + var room = new Room(id, leader, name, password, maxPlayers); + Rooms.Add(id, room); + + RoomSend.Created(leaderId, room); + RoomSend.ListUpdate(); + + Console.WriteLine($"{leader} created a room {room}."); + } + + public static void JoinRoom(Client client, Room room, string password) { + if (room.IsFull) { + RoomSend.JoinFailed(client.Id, room, "Room is full!"); + Console.WriteLine($"{client} tried to join full room {room}."); + } else if (!room.Password.Equals(password)) { + RoomSend.JoinFailed(client.Id, room,"Wrong password!"); + Console.WriteLine($"{client} entered wrong password for room {room}."); + } else { + room.AddClient(client); + RoomSend.Joined(client.Id, room); + RoomSend.ListUpdate(); + Console.WriteLine($"{client} joined the room {room}."); + } + } + + public static void LeaveRoom(Client client) { + Room room = client.Room; + bool isEmpty = !room.RemoveClient(client); + + Console.WriteLine($"{client} has left room {room}"); + + if (isEmpty) { + Rooms.Remove(room.Id); + Console.WriteLine($"Room {room} was deleted because every client left."); + } + + RoomSend.Left(client.Id, room); + RoomSend.ListUpdate(); + } + + public static void KickFromRoom(Client fromClient, Client kickClient) { + Room room = kickClient.Room; + if (!fromClient.Equals(room.Leader)) { + RoomSend.KickFailed(fromClient.Id, "Only the room lead can kick others!"); + Console.WriteLine($"{fromClient} tried to kick {kickClient} while not being the leader!"); + return; + } + + room.KickClient(kickClient); + + Console.WriteLine($"{kickClient} was kicked from room {room}"); + + RoomSend.Left(kickClient.Id, room); + RoomSend.ListUpdate(); + } + + public static void StartRoom(Room room) { + room.IsLocked = true; + room.StartGame(); + + RoomSend.Start(room); + Console.WriteLine($"Room {room} started the game."); + + RoomSend.ListUpdate(); + } + + } + +} diff --git a/Management/ServerHandle.cs b/Management/ServerHandle.cs new file mode 100644 index 0000000..b14e466 --- /dev/null +++ b/Management/ServerHandle.cs @@ -0,0 +1,19 @@ +using System; +using GameServer.Arch; + +namespace GameServer.Management { + public static class ServerHandle { + + public static void WelcomeReceived(int fromClientId, Packet packet) { + string username = packet.ReadString(); + + Client client = Server.Clients[fromClientId]; + client.Name = username; + + Console.WriteLine($"{client} connected successfully!"); + } + + + + } +} diff --git a/Management/ServerSend.cs b/Management/ServerSend.cs new file mode 100644 index 0000000..ff5ec25 --- /dev/null +++ b/Management/ServerSend.cs @@ -0,0 +1,22 @@ +using GameServer.Arch; +using GameServer.PacketTypes; +using static GameServer.Arch.SendData; +using static GameServer.PacketTypes.ServerDefaultPacket; + +namespace GameServer.Management { + internal static class ServerSend { + + private static Packet CreatePacket(ServerDefaultPacket type) { + return new((int)PacketType.Default, (int)type); + } + + public static void Welcome(int toClient, string msg) { + using var packet = CreatePacket(DWelcome); + + packet.Write(msg); + packet.Write(toClient); + + SendTcpData(toClient, packet); + } + } +} diff --git a/PacketTypes/ClientDefaultPacket.cs b/PacketTypes/ClientDefaultPacket.cs new file mode 100644 index 0000000..949a2fa --- /dev/null +++ b/PacketTypes/ClientDefaultPacket.cs @@ -0,0 +1,5 @@ +namespace GameServer.PacketTypes { + public enum ClientDefaultPacket { + DWelcomeReceived = 1, + } +} diff --git a/PacketTypes/ClientGamePacket.cs b/PacketTypes/ClientGamePacket.cs new file mode 100644 index 0000000..c1497c4 --- /dev/null +++ b/PacketTypes/ClientGamePacket.cs @@ -0,0 +1,5 @@ +namespace GameServer.PacketTypes { + public enum ClientGamePacket { + Action, + } +} diff --git a/PacketTypes/ClientRoomPacket.cs b/PacketTypes/ClientRoomPacket.cs new file mode 100644 index 0000000..bbe74b6 --- /dev/null +++ b/PacketTypes/ClientRoomPacket.cs @@ -0,0 +1,13 @@ +namespace GameServer.PacketTypes { + public enum ClientRoomPacket { + RList = 1, + RCreate, + RJoin, + RLeave, + RKick, + RReady, + RColor, + RLeader, + RStart, + } +} diff --git a/PacketTypes/PacketType.cs b/PacketTypes/PacketType.cs new file mode 100644 index 0000000..dbec81a --- /dev/null +++ b/PacketTypes/PacketType.cs @@ -0,0 +1,7 @@ +namespace GameServer.PacketTypes { + public enum PacketType { + Default = 1, + Room, + Game + } +} diff --git a/PacketTypes/ServerDefaultPacket.cs b/PacketTypes/ServerDefaultPacket.cs new file mode 100644 index 0000000..ac4b9db --- /dev/null +++ b/PacketTypes/ServerDefaultPacket.cs @@ -0,0 +1,5 @@ +namespace GameServer.PacketTypes { + public enum ServerDefaultPacket { + DWelcome = 1, + } +} diff --git a/PacketTypes/ServerGamePacket.cs b/PacketTypes/ServerGamePacket.cs new file mode 100644 index 0000000..69da03b --- /dev/null +++ b/PacketTypes/ServerGamePacket.cs @@ -0,0 +1,5 @@ +namespace GameServer.PacketTypes { + public enum ServerGamePacket { + + } +} diff --git a/PacketTypes/ServerRoomPacket.cs b/PacketTypes/ServerRoomPacket.cs new file mode 100644 index 0000000..9c073af --- /dev/null +++ b/PacketTypes/ServerRoomPacket.cs @@ -0,0 +1,14 @@ +namespace GameServer.PacketTypes { + public enum ServerRoomPacket { + RList = 1, + RCreated, + RJoined, + RLeft, + RStart, + RCreateFailed, + RJoinFailed, + RLeaveFailed, + RKickFailed, + RProperties, + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..1c4db8c --- /dev/null +++ b/Program.cs @@ -0,0 +1,21 @@ +using System; +using System.Threading; +using GameServer.Arch; +using GameServer.Management; + +namespace GameServer { + internal static class Program { + private static void Main() { + Console.Title = "Game Server"; + Console.SetOut(new DatePrefix()); + + + Server.Start(50, 26950); + + var mainThread = new Thread(ThreadManager.MainThread); + mainThread.Start(); + + ThreadManager.IsRunning = true; + } + } +}