commit 90b9eae40514d3285a3a696433f557e96792276d Author: techiesplash Date: Sun Jan 15 13:26:28 2023 -0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..1b2e1f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Matchmaker/bin/ +Matchmaker/obj/ \ No newline at end of file diff --git a/.idea/.idea.Matchmaker-API---Server.dir/.idea/.gitignore b/.idea/.idea.Matchmaker-API---Server.dir/.idea/.gitignore new file mode 100755 index 0000000..b8bcebd --- /dev/null +++ b/.idea/.idea.Matchmaker-API---Server.dir/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/modules.xml +/.idea.Matchmaker-API---Server.iml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.Matchmaker-API---Server.dir/.idea/encodings.xml b/.idea/.idea.Matchmaker-API---Server.dir/.idea/encodings.xml new file mode 100755 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.Matchmaker-API---Server.dir/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Matchmaker-API---Server.dir/.idea/indexLayout.xml b/.idea/.idea.Matchmaker-API---Server.dir/.idea/indexLayout.xml new file mode 100755 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.Matchmaker-API---Server.dir/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Matchmaker-API---Server.dir/.idea/misc.xml b/.idea/.idea.Matchmaker-API---Server.dir/.idea/misc.xml new file mode 100755 index 0000000..1d8c84d --- /dev/null +++ b/.idea/.idea.Matchmaker-API---Server.dir/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.Matchmaker-API---Server.dir/.idea/vcs.xml b/.idea/.idea.Matchmaker-API---Server.dir/.idea/vcs.xml new file mode 100755 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.Matchmaker-API---Server.dir/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Matchmaker/.idea/.gitignore b/.idea/.idea.Matchmaker/.idea/.gitignore new file mode 100755 index 0000000..097c37d --- /dev/null +++ b/.idea/.idea.Matchmaker/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.Matchmaker.iml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.Matchmaker/.idea/encodings.xml b/.idea/.idea.Matchmaker/.idea/encodings.xml new file mode 100755 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.Matchmaker/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Matchmaker/.idea/indexLayout.xml b/.idea/.idea.Matchmaker/.idea/indexLayout.xml new file mode 100755 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.Matchmaker/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Matchmaker/.idea/misc.xml b/.idea/.idea.Matchmaker/.idea/misc.xml new file mode 100755 index 0000000..1d8c84d --- /dev/null +++ b/.idea/.idea.Matchmaker/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/.idea.Matchmaker/.idea/vcs.xml b/.idea/.idea.Matchmaker/.idea/vcs.xml new file mode 100755 index 0000000..94a25f7 --- /dev/null +++ b/.idea/.idea.Matchmaker/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.vs/Matchmaker/DesignTimeBuild/.dtbcache.v2 b/.vs/Matchmaker/DesignTimeBuild/.dtbcache.v2 new file mode 100755 index 0000000..b227bcd Binary files /dev/null and b/.vs/Matchmaker/DesignTimeBuild/.dtbcache.v2 differ diff --git a/.vs/Matchmaker/FileContentIndex/f194b12e-6927-4aa2-b7a4-b9e614c2e2a2.vsidx b/.vs/Matchmaker/FileContentIndex/f194b12e-6927-4aa2-b7a4-b9e614c2e2a2.vsidx new file mode 100755 index 0000000..dc4f72e Binary files /dev/null and b/.vs/Matchmaker/FileContentIndex/f194b12e-6927-4aa2-b7a4-b9e614c2e2a2.vsidx differ diff --git a/.vs/Matchmaker/FileContentIndex/fd5857db-c473-4269-9c48-e4bf2f5b2f78.vsidx b/.vs/Matchmaker/FileContentIndex/fd5857db-c473-4269-9c48-e4bf2f5b2f78.vsidx new file mode 100755 index 0000000..5d8210a Binary files /dev/null and b/.vs/Matchmaker/FileContentIndex/fd5857db-c473-4269-9c48-e4bf2f5b2f78.vsidx differ diff --git a/.vs/Matchmaker/FileContentIndex/read.lock b/.vs/Matchmaker/FileContentIndex/read.lock new file mode 100755 index 0000000..e69de29 diff --git a/.vs/Matchmaker/v17/.futdcache.v1 b/.vs/Matchmaker/v17/.futdcache.v1 new file mode 100755 index 0000000..0a5d522 Binary files /dev/null and b/.vs/Matchmaker/v17/.futdcache.v1 differ diff --git a/.vs/Matchmaker/v17/.suo b/.vs/Matchmaker/v17/.suo new file mode 100755 index 0000000..ed9d6f1 Binary files /dev/null and b/.vs/Matchmaker/v17/.suo differ diff --git a/.vs/ProjectEvaluation/matchmaker.metadata.v2 b/.vs/ProjectEvaluation/matchmaker.metadata.v2 new file mode 100755 index 0000000..20296de Binary files /dev/null and b/.vs/ProjectEvaluation/matchmaker.metadata.v2 differ diff --git a/.vs/ProjectEvaluation/matchmaker.projects.v2 b/.vs/ProjectEvaluation/matchmaker.projects.v2 new file mode 100755 index 0000000..c533679 Binary files /dev/null and b/.vs/ProjectEvaluation/matchmaker.projects.v2 differ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100755 index 0000000..7b5c1ea --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + "version": "0.2.0", + "configurations": [ + + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/Matchmaker/bin/Debug/net7.0/Matchmaker.dll", + "args": [], + "cwd": "${workspaceFolder}/Matchmaker", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100755 index 0000000..f33d03d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Matchmaker/Matchmaker.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Matchmaker/Matchmaker.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Matchmaker/Matchmaker.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Images/preview.png b/Images/preview.png new file mode 100755 index 0000000..13e78ff Binary files /dev/null and b/Images/preview.png differ diff --git a/LICENSE.md b/LICENSE.md new file mode 100755 index 0000000..9902a4e --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2020 Tom Weiland +Copyright (c) 2022 Techiesplash + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Matchmaker.sln b/Matchmaker.sln new file mode 100755 index 0000000..a903e7f --- /dev/null +++ b/Matchmaker.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Matchmaker", "Matchmaker\Matchmaker.csproj", "{4C780A67-B2B0-4DE1-AD9A-BD26551A540C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4C780A67-B2B0-4DE1-AD9A-BD26551A540C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C780A67-B2B0-4DE1-AD9A-BD26551A540C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C780A67-B2B0-4DE1-AD9A-BD26551A540C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C780A67-B2B0-4DE1-AD9A-BD26551A540C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Matchmaker.sln.DotSettings.user b/Matchmaker.sln.DotSettings.user new file mode 100755 index 0000000..0d7283e --- /dev/null +++ b/Matchmaker.sln.DotSettings.user @@ -0,0 +1,5 @@ + + True + True + True + True \ No newline at end of file diff --git a/Matchmaker/.vscode/launch.json b/Matchmaker/.vscode/launch.json new file mode 100755 index 0000000..b94c04c --- /dev/null +++ b/Matchmaker/.vscode/launch.json @@ -0,0 +1,26 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net6.0/Matchmaker.dll", + "args": [], + "cwd": "${workspaceFolder}", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/Matchmaker/.vscode/tasks.json b/Matchmaker/.vscode/tasks.json new file mode 100755 index 0000000..fd73f9d --- /dev/null +++ b/Matchmaker/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Matchmaker.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Matchmaker.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Matchmaker.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/Attribute.cs b/Matchmaker/BaseServer/Attribute.cs new file mode 100755 index 0000000..310615b --- /dev/null +++ b/Matchmaker/BaseServer/Attribute.cs @@ -0,0 +1,50 @@ +namespace Matchmaker.Server.BaseServer; + +public class ClientAttributes +{ + private const uint MaxAttributes = 64; + private class Attribute + { + // ReSharper disable once FieldCanBeMadeReadOnly.Local + public string AttributeName; + public string AttributeValue; + + public Attribute(string name, string value) + { + AttributeName = name; + AttributeValue = value; + } + } + + private readonly List _attributes = new(); + + public void SetAttribute(string name, string value) + { + // Check if the attribute is already set + foreach (var attr in _attributes.Where(attr => attr.AttributeName.Equals(name))) + { + attr.AttributeValue = value; + return; + } + + // If it is not + if (_attributes.Count < MaxAttributes) + { + _attributes.Add(new Attribute(name, value)); + } + else + { + Console.WriteLine("Failed to add attribute: Reached limit!"); + } + } + + public string? GetAttribute(string name) + { + return _attributes.Where(attr => attr.AttributeName.Equals(name)).Select(attr => attr.AttributeValue).FirstOrDefault(); + } + + public void Clear() + { + _attributes.Clear(); + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/Client.cs b/Matchmaker/BaseServer/Client.cs new file mode 100755 index 0000000..ebc3a5f --- /dev/null +++ b/Matchmaker/BaseServer/Client.cs @@ -0,0 +1,306 @@ +using System.Net; +using System.Net.Sockets; + +namespace Matchmaker.Server.BaseServer; + +public class Client +{ + public ServerSend.StatusType LastStatus; + + public readonly ClientAttributes Attributes = new(); + + public readonly TCP? Tcp; + public readonly UDP Udp; + public readonly Server ServerParent; + + public Client(int clientId, Server server) + { + Tcp = new TCP(clientId, server); + Udp = new UDP(clientId, server); + ServerParent = server; + } + + public bool IsConnected + { + get + { + try + { + if (Tcp?.Socket != null && Tcp.Socket.Client != null && Tcp.Socket.Client.Connected) + { + /* pear to the documentation on Poll: + * When passing SelectMode.SelectRead as a parameter to the Poll method it will return + * -either- true if Socket.Listen(Int32) has been called and a connection is pending; + * -or- true if data is available for reading; + * -or- true if the connection has been closed, reset, or terminated; + * otherwise, returns false + */ + + // Detect if client disconnected + if (Tcp.Socket.Client.Poll(0, SelectMode.SelectRead)) + { + byte[] buff = new byte[1]; + if (Tcp.Socket.Client.Receive(buff, SocketFlags.Peek) == 0) + { + // Client disconnected + return false; + } + else + { + return true; + } + } + + return true; + } + else + { + return false; + } + } + catch + { + return false; + } + } + } + + // ReSharper disable once InconsistentNaming + public class TCP + { + private readonly Server _serv; + public TcpClient? Socket; + + private readonly int _id; + private NetworkStream? _stream; + private Packet? _receivedData; + private byte[]? _receiveBuffer; + + public TCP(int id, Server server) + { + _serv = server; + _id = id; + } + + + public void Connect(TcpClient? socket) + { + Terminal.LogDebug("Connecting TCP..."); + Socket = socket; + if (Socket != null) + { + 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); + + ServerSend.Welcome(_serv, _id, "Welcome to the server!"); + } + + public void SendData(Packet packet) + { + try + { + if (Socket != null) + { + _stream?.BeginWrite(packet.ToArray(), 0, packet.Length(), null, null); + } + } + catch (Exception ex) + { + Terminal.LogError($"Error sending data to player {_id} via TCP: {ex}"); + } + } + + private void ReceiveCallback(IAsyncResult result) + { + try + { + if (Socket?.Client.RemoteEndPoint is not IPEndPoint remoteIpEndPoint) + { + Terminal.LogDebug($"[{_serv.DisplayName}]: RemoteIpEndPoint is NULL!"); + _serv.Clients[_id].Attributes.Clear(); + _serv.Clients[_id].Disconnect(); + return; + } + + if (_stream != null) + { + var byteLength = _stream.EndRead(result); + if (byteLength <= 0) + { + Terminal.LogDebug("Client has left. Disconnecting..."); + _serv.DisconnectClient(_id); + return; + } + + var data = new byte[byteLength]; + if (_receiveBuffer != null) Array.Copy(_receiveBuffer, data, byteLength); + + _receivedData?.Reset(HandleData(data)); + } + + if (_receiveBuffer != null) + { + _stream?.BeginRead(_receiveBuffer, 0, Constants.DataBufferSize, ReceiveCallback, null); + } + } + catch (Exception ex) + { + Terminal.LogError($"[{_serv.DisplayName}] Error receiving TCP data: {ex}"); + _serv.DisconnectClient(_id); + } + } + + private bool HandleData(IEnumerable 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(() => + { + try + { + var packet = new Packet(packetBytes); + + var packetId = packet.ReadInt(); + if ((packetId != int.MaxValue - 3) && Config.Sync) + { + ServerSend.Status(_serv, _id, ServerSend.StatusType.RECEIVED); + } + + Terminal.LogDebug($"[{_serv.DisplayName}] Received TCP Packet with ID: " + packetId); + var x = _serv.Packets?.PacketHandlers.ContainsKey(packetId); + if (x != null && x != false) + { + _serv.Packets?.PacketHandlers[packetId].DynamicInvoke(_serv, _id, packet); + } + else + { + Terminal.LogError( + $"[{_serv.DisplayName}] Received unregistered TCP packet with ID: {packetId}"); + } + } + catch (Exception e) + { + Terminal.LogError($"[{_serv.DisplayName}] Error processing packet: {e}"); + throw; + } + }); + + 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; + } + } + + // ReSharper disable once InconsistentNaming + public class UDP + { + public IPEndPoint? EndPoint; + private readonly Server _serv; + private readonly int _id; + + public UDP(int id, Server server) + { + _serv = server; + _id = id; + } + + public void Connect(IPEndPoint endPoint) + { + EndPoint = endPoint; + } + + public void SendData(Packet packet) + { + if (EndPoint != null) _serv.SendUdpData(EndPoint, packet); + } + + public void HandleData(Packet packetData) + { + var packetLength = packetData.ReadInt(); + var packetBytes = packetData.ReadBytes(packetLength); + + ThreadManager.ExecuteOnMainThread(() => + { + try + { + using var packet = new Packet(packetBytes); + var packetId = packet.ReadInt(); + if ((packetId != int.MaxValue - 3) && Config.Sync) + { + ServerSend.Status(_serv, _id, ServerSend.StatusType.RECEIVED); + } + + Terminal.LogDebug($"[{_serv.DisplayName}] Received UDP Packet with ID: " + packetId); + var x = _serv.Packets?.PacketHandlers.ContainsKey(packetId); + if (x != null && x != false) + { + _serv.Packets?.PacketHandlers[packetId].DynamicInvoke(_serv, _id, packet); + } + else + { + Terminal.LogError( + $"[{_serv.DisplayName}] Received unregistered UDP packet with ID: {packetId}"); + } + } + catch (Exception e) + { + Terminal.LogError($"[{_serv.DisplayName}] Error processing packet: {e}"); + throw; + } + }); + } + + public void Disconnect() + { + EndPoint = null; + } + } + + public void Disconnect() + { + Terminal.LogInfo( + $"[{ServerParent.DisplayName}] Disconnecting Client ({Tcp?.Socket?.Client.RemoteEndPoint})..."); + + Tcp?.Disconnect(); + Udp.Disconnect(); + Attributes.Clear(); + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/Constants.cs b/Matchmaker/BaseServer/Constants.cs new file mode 100755 index 0000000..a214b25 --- /dev/null +++ b/Matchmaker/BaseServer/Constants.cs @@ -0,0 +1,12 @@ + + +namespace Matchmaker.Server.BaseServer; + +internal static class Constants +{ + public const int TicksPerSec = 30; + public const float MsPerTick = 1000f / TicksPerSec; + public const int DataBufferSize = 4096; + + +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/GameLogic.cs b/Matchmaker/BaseServer/GameLogic.cs new file mode 100755 index 0000000..30b0251 --- /dev/null +++ b/Matchmaker/BaseServer/GameLogic.cs @@ -0,0 +1,12 @@ + + +namespace Matchmaker.Server.BaseServer; + +internal static class GameLogic +{ + public static void Update() + { + ThreadManager.UpdateMain(); + + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/Packet.cs b/Matchmaker/BaseServer/Packet.cs new file mode 100755 index 0000000..0b3cfb3 --- /dev/null +++ b/Matchmaker/BaseServer/Packet.cs @@ -0,0 +1,398 @@ + + +using System.Numerics; +using System.Text; + +namespace Matchmaker.Server.BaseServer; + +public sealed class Packet : IDisposable +{ + private List _buffer; + private byte[] _readableBuffer = null!; + 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 packet ID. + public Packet(int id) + { + _buffer = new List(); // Initialize buffer + _readPos = 0; // Set readPos to 0 + + Write(id); // Write packet id to the buffer + } + + /// Creates a packet from which data can be read. Used for receiving. + /// The bytes to add to the packet. + public Packet(IEnumerable data) + { + _buffer = new List(); // Initialize buffer + _readPos = 0; // Set readPos to 0 + + SetBytes(data); + } + + #region Functions + /// Sets the packet's content and prepares it to be read. + /// The bytes to add to the packet. + public void SetBytes(IEnumerable 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. + // ReSharper disable once UnusedMember.Global + 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. + // ReSharper disable once UnusedMember.Global + public void Write(byte value) + { + _buffer.Add(value); + } + /// Adds an array of bytes to the packet. + /// The byte array to add. + // ReSharper disable once MemberCanBePrivate.Global + public void Write(IEnumerable value) + { + _buffer.AddRange(value); + } + /// Adds a short to the packet. + /// The short to add. + // ReSharper disable once UnusedMember.Global + 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. + // ReSharper disable once UnusedMember.Global + public void Write(long value) + { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + /// Adds a float to the packet. + /// The float to add. + // ReSharper disable once UnusedMember.Global + public void Write(float value) + { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + /// Adds a bool to the packet. + /// The bool to add. + // ReSharper disable once UnusedMember.Global + public void Write(bool value) + { + _buffer.AddRange(BitConverter.GetBytes(value)); + } + /// Adds a string to the packet. + /// The string to add. + // ReSharper disable once UnusedMember.Global + 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. + // ReSharper disable once UnusedMember.Global + public void Write(Vector3 value) + { + Write(value.X); + Write(value.Y); + Write(value.Z); + } + /// Adds a Quaternion to the packet. + /// The Quaternion to add. + // ReSharper disable once UnusedMember.Global + public void Write(Quaternion value) + { + Write(value.X); + Write(value.Y); + Write(value.Z); + Write(value.W); + } + #endregion + + #region Read Data + /// Reads a byte from the packet. + /// Whether or not to move the buffer's read position. + // ReSharper disable once UnusedMember.Global + 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 + } + // ReSharper disable once RedundantIfElseBlock + else + { + 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 + } + + // ReSharper disable once RedundantIfElseBlock + else + { + 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. + // ReSharper disable once UnusedMember.Global + 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 + } + // ReSharper disable once RedundantIfElseBlock + else + { + 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 + } + // ReSharper disable once RedundantIfElseBlock + else + { + 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. + // ReSharper disable once UnusedMember.Global + 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 + } + // ReSharper disable once RedundantIfElseBlock + else + { + 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 + } + // ReSharper disable once RedundantIfElseBlock + else + { + 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. + // ReSharper disable once UnusedMember.Global + 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 + } + // ReSharper disable once RedundantIfElseBlock + else + { + 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. + // ReSharper disable once UnusedMember.Global + public Vector3 ReadVector3(bool moveReadPos = true) + { + return new Vector3(ReadFloat(moveReadPos), ReadFloat(moveReadPos), ReadFloat(moveReadPos)); + } + + /// Reads a Quaternion from the packet. + /// Whether or not to move the buffer's read position. + // ReSharper disable once UnusedMember.Global + public Quaternion ReadQuaternion(bool moveReadPos = true) + { + return new Quaternion(ReadFloat(moveReadPos), ReadFloat(moveReadPos), ReadFloat(moveReadPos), ReadFloat(moveReadPos)); + } + #endregion + + private bool _disposed; + + private void Dispose(bool disposing) + { + if (_disposed) return; + if (disposing) + { + _buffer = null!; + _readableBuffer = null!; + _readPos = 0; + } + + _disposed = true; + } + + public void Dispose() + { + Dispose(true); + // ReSharper disable once GCSuppressFinalizeForTypeWithoutDestructor + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/Server.cs b/Matchmaker/BaseServer/Server.cs new file mode 100755 index 0000000..4cd16e8 --- /dev/null +++ b/Matchmaker/BaseServer/Server.cs @@ -0,0 +1,159 @@ + + +using System.Net; +using System.Net.Sockets; + +namespace Matchmaker.Server.BaseServer; + +public class Server +{ + public int MaxPlayers; + private int _port; + public readonly Dictionary Clients = new(); + + public string DisplayName = "Unnamed Server"; + + public class ServerPackets + { + public delegate void PacketHandler(Server server, int fromClient, Packet packet); + + // ReSharper disable once FieldCanBeMadeReadOnly.Global + public Dictionary PacketHandlers; + + public ServerPackets(Dictionary packetHandlers) + { + PacketHandlers = packetHandlers; + PacketHandlers.Add(int.MaxValue, ServerHandle.WelcomeReceived); + PacketHandlers.Add(int.MaxValue - 1, ServerHandle.SetClientAttribute); + PacketHandlers.Add(int.MaxValue - 2, ServerHandle.GetClientAttribute); + } + } + + public ServerPackets? Packets; + /* + public Dictionary packetHandlers = new Dictionary() + { + { (int)ClientPackets.welcomeReceived, ServerHandle.WelcomeReceived }, + { (int)ClientPackets.setClientAttributes, ServerHandle.SetClientAttribute }, + { (int)ClientPackets.getClientAttributes, ServerHandle.GetClientAttribute } + }; + */ + private TcpListener _tcpListener = null!; + + private UdpClient _udpListener = null!; + + // ReSharper disable once UnusedMethodReturnValue.Global + public bool Start(ServerPackets packets, int maxPlayers, int port, string displayName) + { + try + { + DisplayName = displayName; + + Packets = packets; + + MaxPlayers = maxPlayers; + _port = port; + + InitializeServerData(); + + _tcpListener = new TcpListener(IPAddress.Any, _port); + _tcpListener.Start(); + _tcpListener.BeginAcceptTcpClient(TcpConnectCallback, null); + + _udpListener = new UdpClient(_port); + _udpListener.BeginReceive(UdpReceiveCallback, null); + + Terminal.LogSuccess($"[{DisplayName}] Server started on port {_port}."); + return true; + } + catch (Exception ex) + { + Terminal.LogError($"[{DisplayName}] Failed to start server: {ex}"); + return false; + } + } + + public void DisconnectClient(int id) + { + Clients[id].Disconnect(); + } + + + private void TcpConnectCallback(IAsyncResult result) + { + var client = _tcpListener.EndAcceptTcpClient(result); + _tcpListener.BeginAcceptTcpClient(TcpConnectCallback, null); + Terminal.LogInfo($"[{DisplayName}] Incoming connection from {client.Client.RemoteEndPoint}..."); + + for (var i = 1; i <= MaxPlayers; i++) + { + if (Clients[i].Tcp!.Socket != null) continue; + Clients[i].Tcp!.Connect(client); + return; + } + + Terminal.LogWarn($"[{DisplayName}] {client.Client.RemoteEndPoint} failed to connect: Server full!"); + } + + private 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; + } + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + 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) + { + Terminal.LogError($"[{DisplayName}] Error receiving UDP data: {ex}"); + } + } + + public void SendUdpData(IPEndPoint clientEndPoint, Packet packet) + { + try + { + _udpListener.BeginSend(packet.ToArray(), packet.Length(), clientEndPoint, null, null); + } + catch (Exception ex) + { + Terminal.LogError($"[{DisplayName}] Error sending data to {clientEndPoint} via UDP: {ex}"); + } + } + + private void InitializeServerData() + { + for (var i = 1; i <= MaxPlayers; i++) + { + Clients.Add(i, new Client(i, this)); + } + + Terminal.LogSuccess($"[{DisplayName}] Server data is now initialized."); + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/ServerHandle.cs b/Matchmaker/BaseServer/ServerHandle.cs new file mode 100755 index 0000000..4233de3 --- /dev/null +++ b/Matchmaker/BaseServer/ServerHandle.cs @@ -0,0 +1,156 @@ +using System.Net; + +// ReSharper disable once CheckNamespace +namespace Matchmaker.Server.BaseServer; + +internal static class ServerHandle +{ + /// + /// Gets the message received in response to the Welcome message + /// + /// + /// + /// + public static void WelcomeReceived(Server server, int fromClient, Packet packet) + { + try + { + var clientIdCheck = packet.ReadInt(); + var clientAPIVersion = packet.ReadString(); + var clientGameVersion = packet.ReadString(); + var clientGameId = packet.ReadString(); + + if (clientAPIVersion != Config.MatchmakerAPIVersion) + { + Terminal.LogError( + $"[{server.DisplayName}] Client API is not the same version as the Server! \n Server API version: {Config.MatchmakerAPIVersion}. \n Client API version: {clientAPIVersion}"); + server.Clients[fromClient].Disconnect(); + return; + } + + if (clientGameId != Config.GameId) + { + Terminal.LogError($"Client Game ID and Server Game ID do not match: \n Server Game ID: {Config.GameId}. \n Client Game ID: {clientGameId}."); + server.Clients[fromClient].Disconnect(); + return; + } + + if (clientGameVersion != Config.GameVersion) + { + Terminal.LogError($"Client Game Version and Server Game Version do not match: \n Server Game Ver: {Config.GameVersion}. \n Client Game Ver: {clientGameVersion}."); + server.Clients[fromClient].Disconnect(); + return; + } + + var tcp = server.Clients[fromClient].Tcp; + if (tcp == null) + { + Terminal.LogWarn("TCP is null!"); + } + + if (tcp is { Socket: { } }) + Terminal.LogSuccess( + $"[{server.DisplayName}] {tcp.Socket.Client.RemoteEndPoint} connected successfully and is now player {fromClient}."); + if (fromClient != clientIdCheck) + { + Terminal.LogWarn( + $"[{server.DisplayName}] Player (ID: {fromClient}) has assumed the wrong client ID ({clientIdCheck})!"); + } + + + var remoteIpEndPoint = server.Clients[fromClient].Tcp?.Socket?.Client.RemoteEndPoint as IPEndPoint; + if (remoteIpEndPoint == null) + { + Terminal.LogDebug($"[{server.DisplayName}] WelcomeReceived: RemoteIpEndPoint is NULL!"); + server.Clients[fromClient].Attributes.Clear(); + server.Clients[fromClient].Disconnect(); + return; + } + + var uuidList = new List(); + for (var i = 1; i < server.Clients.Count; i++) + { + if (server.Clients[i].IsConnected) + { + var x = server.Clients[i].Attributes.GetAttribute("_auto_uuid"); + if (x != null) + { + uuidList.Add(x); + } + } + } + + + server.Clients[fromClient].Attributes.SetAttribute("_auto_ip", remoteIpEndPoint.Address.ToString()); + server.Clients[fromClient].Attributes.SetAttribute("_auto_port", remoteIpEndPoint.Port.ToString()); + server.Clients[fromClient].Attributes + .SetAttribute("_auto_uuid", new UUID(Config.UuidLength, uuidList).GetValue()); + server.Clients[fromClient].Attributes + .SetAttribute("_auto_game_version", clientGameVersion); + + Terminal.LogInfo( + $"[{server.DisplayName}] Automatically assigned _auto_ip for client {fromClient}. _auto_ip = {server.Clients[fromClient].Attributes.GetAttribute("_auto_ip")}"); + Terminal.LogInfo( + $"[{server.DisplayName}] Automatically assigned _auto_port for client {fromClient}. _auto_port = {server.Clients[fromClient].Attributes.GetAttribute("_auto_port")}"); + Terminal.LogInfo( + $"[{server.DisplayName}] Automatically assigned _auto_uuid for client {fromClient}. _auto_uuid = {server.Clients[fromClient].Attributes.GetAttribute("_auto_uuid")}"); + Terminal.LogInfo( + $"[{server.DisplayName}] Automatically assigned _auto_game_version for client {fromClient} based on received Client data. _auto_game_version = {server.Clients[fromClient].Attributes.GetAttribute("_auto_game_version")}"); + } + catch (Exception ex) + { + Terminal.LogError($"[{server.DisplayName}] Error in WelcomeReceived packet handler: {ex}"); + ServerSend.Status(server, fromClient, ServerSend.StatusType.FAIL); + server.Clients[fromClient].Disconnect(); + } + } + + public static void SetClientAttribute(Server server, int fromClient, Packet packet) + { + var clientIdCheck = packet.ReadInt(); + var name = packet.ReadString(); + var value = packet.ReadString(); + + var tcp = server.Clients[fromClient].Tcp; + if (tcp is { Socket: { } }) + Terminal.LogInfo( + $"[{server.DisplayName}] {tcp.Socket.Client.RemoteEndPoint} is changing Client Attribute {name} to {value}."); + if (fromClient != clientIdCheck) + { + Terminal.LogWarn( + $"[{server.DisplayName}] Player (ID: {fromClient}) has assumed the wrong client ID ({clientIdCheck})!"); + + return; + } + + server.Clients[fromClient].Attributes.SetAttribute(name, value); + } + + public static void GetClientAttribute(Server server, int fromClient, Packet packet) + { + var clientIdCheck = packet.ReadInt(); + var requestedId = packet.ReadInt(); + var name = packet.ReadString(); + + try + { + var tcp = server.Clients[fromClient].Tcp; + if (tcp is { Socket: { } }) + Terminal.LogInfo( + $"[{server.DisplayName}] {tcp.Socket.Client.RemoteEndPoint} requests Client Attribute {name}."); + if (fromClient != clientIdCheck) + { + Terminal.LogWarn( + $"[{server.DisplayName}] Player (ID: {fromClient}) has assumed the wrong client ID ({clientIdCheck})!"); + } + + var value = server.Clients[requestedId].Attributes.GetAttribute(name); + ServerSend.GetClientAttributesReceived(server, fromClient, requestedId, name, value ?? ""); + } + catch (Exception ex) + { + Terminal.LogError($"[{server.DisplayName}] Error in GetClientAttribute: {ex}"); + ServerSend.GetClientAttributesReceived(server, fromClient, requestedId, name, "ERR_HANDLED_EXCEPTION"); + } + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/ServerSend.cs b/Matchmaker/BaseServer/ServerSend.cs new file mode 100755 index 0000000..914ef09 --- /dev/null +++ b/Matchmaker/BaseServer/ServerSend.cs @@ -0,0 +1,277 @@ + + + +namespace Matchmaker.Server.BaseServer; + +public static class ServerSend +{ + + public enum StatusType + { + OK, + RECEIVED, + FAIL + } + + + /// + /// Sends TCP data to a Client + /// + /// Server the Client is in + /// ID for the recipient + /// Data to send + public static void SendTcpData(Server server, int toClient, Packet packet) + { + packet.WriteLength(); + var tcp = server.Clients[toClient].Tcp; + tcp?.SendData(packet); + } + + /// + /// Sends UDP data to a Client + /// + /// Server the Client is in + /// ID for the recipient + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendUdpData(Server server, int toClient, Packet packet) + { + packet.WriteLength(); + var udp = server.Clients[toClient].Udp; + udp.SendData(packet); + } + + /// + /// Sends TCP data to all Clients + /// + /// Server the Client is in + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendTcpDataToAll(Server server, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + var tcp = server.Clients[i].Tcp; + tcp?.SendData(packet); + } + } + + /// + /// Sends TCP data to all Clients except one + /// + /// Server the Client is in + /// /// What Client to exclude + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendTcpDataToAll(Server server, int exceptClient, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + if (i == exceptClient) continue; + var tcp = server.Clients[i].Tcp; + tcp?.SendData(packet); + } + } + + /// + /// Sends UDP data to all Clients + /// + /// Server the Client is in + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendUdpDataToAll(Server server, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + var udp = server.Clients[i].Udp; + udp.SendData(packet); + } + } + + + /// + /// Sends UDP data to all Clients except one + /// + /// Server the Client is in + /// What Client to exclude + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendUdpDataToAll(Server server, int exceptClient, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + if (i == exceptClient) continue; + var udp = server.Clients[i].Udp; + udp.SendData(packet); + } + } + + /// + /// Sends TCP data to a Client + /// + /// Server the Client is in + /// ID for the recipient + /// Data to send + public static void SendTcpDataNoSync(Server server, int toClient, Packet packet) + { + packet.WriteLength(); + var tcp = server.Clients[toClient].Tcp; + tcp?.SendData(packet); + } + + /// + /// Sends UDP data to a Client + /// + /// Server the Client is in + /// ID for the recipient + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendUdpDataNoSync(Server server, int toClient, Packet packet) + { + packet.WriteLength(); + var udp = server.Clients[toClient].Udp; + udp.SendData(packet); + } + + /// + /// Sends TCP data to all Clients + /// + /// Server the Client is in + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendTcpDataToAllNoSync(Server server, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + var tcp = server.Clients[i].Tcp; + tcp?.SendData(packet); + } + } + + /// + /// Sends TCP data to all Clients except one + /// + /// Server the Client is in + /// /// What Client to exclude + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendTcpDataToAllNoSync(Server server, int exceptClient, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + if (i == exceptClient) continue; + var tcp = server.Clients[i].Tcp; + tcp?.SendData(packet); + } + } + + /// + /// Sends UDP data to all Clients + /// + /// Server the Client is in + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendUdpDataToAllNoSync(Server server, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + var udp = server.Clients[i].Udp; + udp.SendData(packet); + } + } + + + /// + /// Sends UDP data to all Clients except one + /// + /// Server the Client is in + /// What Client to exclude + /// Data to send + // ReSharper disable once UnusedMember.Global + public static void SendUdpDataToAllNoSync(Server server, int exceptClient, Packet packet) + { + packet.WriteLength(); + for (var i = 1; i <= server.MaxPlayers; i++) + { + if (i == exceptClient) continue; + var udp = server.Clients[i].Udp; + udp.SendData(packet); + } + } + + #region Built-in Packets + + /// + /// Welcome message for Client + /// + /// Server the Client is in + /// ID for the recipient + /// Message to send + public static void Welcome(Server server, int toClient, string msg) + { + Terminal.LogDebug($"[{server.DisplayName}] Sending 'Welcome' message."); + using var packet = new Packet(int.MaxValue); + packet.Write(msg); + packet.Write(toClient); + packet.Write(Config.MatchmakerAPIVersion); + packet.Write(Config.GameId); + packet.Write(Config.GameVersion); + + SendTcpData(server, toClient, packet); + } + + /// + /// Disconnect a Client + /// + /// What server the Client is in + /// What Client to kick + public static void DisconnectClient(Server server, int toClient) + { + using var packet = new Packet(int.MaxValue-1); + SendTcpData(server, toClient, packet); + } + + /// + /// Send an Attribute to the Client + /// + /// What server the Client is in + /// ID for the recipient + /// What Client the Attribute is from + /// The Attribute name + /// The Attribute value + public static void GetClientAttributesReceived(BaseServer.Server server, int toClient, int requestedId, string name, string value) + { + using var packet = new Packet(int.MaxValue-2); + packet.Write(requestedId); + packet.Write(name); + packet.Write(value); + + ServerSend.SendTcpData(server, toClient, packet); + } + + /// + /// Respond to the client with a status + /// + /// What server the Client is in + /// ID for the recipient + /// What status + public static void Status(BaseServer.Server server, int toClient, StatusType status) + { + Terminal.LogDebug($"[{server.DisplayName}] Sending status {status}..."); + using var packet = new Packet(int.MaxValue-3); + packet.Write((int)status); + + ServerSend.SendUdpDataNoSync(server, toClient, packet); + } + + + + #endregion +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/Terminal.cs b/Matchmaker/BaseServer/Terminal.cs new file mode 100755 index 0000000..d9f830f --- /dev/null +++ b/Matchmaker/BaseServer/Terminal.cs @@ -0,0 +1,98 @@ +/* + * MIT License + * + * Copyright (c) 2022 Vincent Dowling + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +namespace Matchmaker.Server.BaseServer; + +public static class Terminal +{ + // ReSharper disable once UnusedMember.Global + public static void Log(string str) + { + Console.ForegroundColor = ConsoleColor.White; + Console.Write(Config.LogText); + Console.ForegroundColor = ConsoleColor.White; + + Console.Write("({0:t}) " + str + "\n", DateTime.Now); + Console.ForegroundColor = ConsoleColor.White; + } + + // ReSharper disable once UnusedMember.Global + public static void LogError(string str) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.Write(Config.LogErrorText); + Console.ForegroundColor = ConsoleColor.White; + + Console.Write("({0:t}) " + str + "\n", DateTime.Now); + Console.ForegroundColor = ConsoleColor.White; + } + + // ReSharper disable once UnusedMember.Global + public static void LogDebug(string str) + { + + if (Config.ShowTerminalDebug) + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.Write(Config.LogDebugText); + Console.ForegroundColor = ConsoleColor.White; + + Console.Write("({0:t}) " + str + "\n", DateTime.Now); + Console.ForegroundColor = ConsoleColor.White; + } + } + + // ReSharper disable once UnusedMember.Global + public static void LogWarn(string str) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.Write(Config.LogWarningText); + Console.ForegroundColor = ConsoleColor.White; + + Console.Write("({0:t}) " + str + "\n", DateTime.Now); + Console.ForegroundColor = ConsoleColor.White; + } + + // ReSharper disable once UnusedMember.Global + public static void LogSuccess(string str) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.Write(Config.LogSuccessText); + Console.ForegroundColor = ConsoleColor.White; + + Console.Write("({0:t}) " + str + "\n", DateTime.Now); + Console.ForegroundColor = ConsoleColor.White; + } + + // ReSharper disable once UnusedMember.Global + public static void LogInfo(string str) + { + Console.ForegroundColor = ConsoleColor.Blue; + Console.Write(Config.LogInfoText); + Console.ForegroundColor = ConsoleColor.White; + + Console.Write("({0:t}) " + str + "\n", DateTime.Now); + Console.ForegroundColor = ConsoleColor.White; + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/ThreadManager.cs b/Matchmaker/BaseServer/ThreadManager.cs new file mode 100755 index 0000000..2b2dd42 --- /dev/null +++ b/Matchmaker/BaseServer/ThreadManager.cs @@ -0,0 +1,93 @@ + +namespace Matchmaker.Server.BaseServer; + +internal static class ThreadManager +{ + private static readonly List ExecuteOnMainThreadList = new(); + private static readonly List ExecuteCopiedOnMainThread = new(); + private static bool _actionToExecuteOnMainThread; + + /// + /// Controls whether or not the Main Thread should run. Set to FALSE to shut down Main Thread. + /// + private static bool _isRunning; + private static Thread? _thread; + + + /// 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) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (action == null) + { + Terminal.LogWarn("No action to execute on main thread!"); + return; + } + + lock (ExecuteOnMainThreadList) + { + ExecuteOnMainThreadList.Add(action); + _actionToExecuteOnMainThread = true; + } + } + + /// + /// Start the Main Thread so Packets can be acted upon + /// + public static void Start() + { + _isRunning = true; + _thread = new Thread(MainThread); + _thread.Start(); + } + + /// + /// Stop the Main Thread + /// + public static void Stop() + { + _isRunning = false; + } + + /// + /// Executes all code meant to run on the main thread. NOTE: Call this ONLY from the main thread. + /// + public static void UpdateMain() + { + if (!_actionToExecuteOnMainThread) return; + ExecuteCopiedOnMainThread.Clear(); + lock (ExecuteOnMainThreadList) + { + ExecuteCopiedOnMainThread.AddRange(ExecuteOnMainThreadList); + ExecuteOnMainThreadList.Clear(); + _actionToExecuteOnMainThread = false; + } + + foreach (var t in ExecuteCopiedOnMainThread) + { + t(); + } + } + + private static void MainThread() + { + + var nextLoop = DateTime.Now; + + while (_isRunning) + { + while (nextLoop < DateTime.Now) + { + GameLogic.Update(); + + nextLoop = nextLoop.AddMilliseconds(Constants.MsPerTick); + + if (nextLoop > DateTime.Now) + { + Thread.Sleep(nextLoop - DateTime.Now); + } + } + } + } +} \ No newline at end of file diff --git a/Matchmaker/BaseServer/UUID.cs b/Matchmaker/BaseServer/UUID.cs new file mode 100755 index 0000000..91afe2a --- /dev/null +++ b/Matchmaker/BaseServer/UUID.cs @@ -0,0 +1,69 @@ +namespace Matchmaker.Server.BaseServer; + +/// +/// A UUID Object +/// +public class UUID +{ + private string value = ""; + + /// + /// Generates a random string of specified length + /// + /// How long a random string to make + /// A random string + public string GetRandomString(int length) + { + // Creating object of random class + var rand = new Random(); + + + var str = ""; + + for (var i = 0; i < length; i++) + { + + // Generating a random number. + var randValue = rand.Next(0, 26); + + // Generating random character by converting + // the random number into character. + var letter = Convert.ToChar(randValue + 65); + + // Appending the letter to string. + str = str + letter; + } + + return str; + } + + /// + /// Get the UUID value + /// + /// The UUID's value + public string GetValue() + { + return value; + } + + public UUID(int length, List usedIDs) + { + var tempValue = ""; + var loop = true; + + while (loop) + { + tempValue = GetRandomString(length); + loop = false; + foreach (var e in usedIDs) + { + if (e.Equals(tempValue)) + { + loop = true; + } + } + } + + value = tempValue; + } +} \ No newline at end of file diff --git a/Matchmaker/Config.cs b/Matchmaker/Config.cs new file mode 100755 index 0000000..088cf50 --- /dev/null +++ b/Matchmaker/Config.cs @@ -0,0 +1,90 @@ +namespace Matchmaker.Server; + +public static class Config +{ + /// + /// This allows you to name the server so your game cannot connect to other ones. + /// Use the game name here and on the client as well. + /// + public const string GameId = "My Game Name"; + + /// + /// The version of the matchmaker API. This must match here and on the client for a connection. + /// + public const string MatchmakerAPIVersion = "1.1.6"; + + /// + /// What version the game is. This must match here and on the client for a connection. + /// + public const string GameVersion = "0.0.1"; + + /// + /// How many people can be connected to the List server instance at the same time. + /// + public const ushort ListMaxClients = 512; + + /// + /// The maximum amount of Lobbies. + /// + public const ushort MaxLobbies = 1024; + + /// + /// What port the server will be on (0-65535). The lobby server will use a port two (2) above this. + /// + public const ushort Port = 26950; + + /// + /// The display name for the List server instance, if you want to change it. + /// + public const string ListServerDisplayName = "List Server"; + + /// + /// The display name for the Lobby server instance, if you want to change it. + /// + public const string LobbyServerDisplayName = "Lobby Server"; + + /// + /// How long generated UUIDs are + /// + public const int UuidLength = 7; + + /// + /// Sends a response to the Client whenever a message is received. Slow, for debugging. + /// + public const bool Sync = false; + + /// + /// Whether or not to show LogDebug() text. + /// + public const bool ShowTerminalDebug = true; + + /// + /// Text that is prepended to a Log() text. + /// + public const string LogText = " LOG "; + + /// + /// Text that is prepended to a LogDebug() text. + /// + public const string LogDebugText = " DEBUG "; + + /// + /// Text that is prepended to a LogInfo() text. + /// + public const string LogInfoText = " CHECK "; + + /// + /// Text that is prepended to a LogWarn() text. + /// + public const string LogWarningText = "WARNING "; + + /// + /// Text that is prepended to a LogError() text. + /// + public const string LogErrorText = " ERROR "; + + /// + /// Text that is prepended to a LogSuccess() text. + /// + public const string LogSuccessText = "SUCCESS "; +} \ No newline at end of file diff --git a/Matchmaker/Matchmaker.csproj b/Matchmaker/Matchmaker.csproj new file mode 100755 index 0000000..2b14c81 --- /dev/null +++ b/Matchmaker/Matchmaker.csproj @@ -0,0 +1,10 @@ + + + + Exe + net7.0 + enable + enable + + + diff --git a/Matchmaker/ServerPackets.cs b/Matchmaker/ServerPackets.cs new file mode 100755 index 0000000..2d7d7e0 --- /dev/null +++ b/Matchmaker/ServerPackets.cs @@ -0,0 +1,159 @@ +using Matchmaker.Server.BaseServer; + +namespace Matchmaker.Server; + +public static class Packets +{ + public static class PacketHandlers + { + /// + /// Handle for if a client requests all IDs in the Lobby server database + /// + /// What server the Client is in + /// What Client the request is from + /// The data + public static void RequestAllLobbyIDs(BaseServer.Server server, int fromClient, Packet packet) + { + var clientIdCheck = packet.ReadInt(); + + var tcp = server.Clients[fromClient].Tcp; + if (tcp is { Socket: { } }) + Terminal.LogInfo( + $"[{server.DisplayName}] Client {fromClient} has requested to cycle through all available lobbies."); + + if (fromClient != clientIdCheck) + { + Terminal.LogWarn( + $"[{server.DisplayName}] [RequestAllLobbyIDs] Player (ID: {fromClient}) has assumed the wrong client ID ({clientIdCheck})!"); + } + + for (var i = 1; i <= Program.LobbyServ!.Clients.Count; i++) + { + if (Program.LobbyServ.Clients[i].IsConnected) + { + ServerPackets.SendLobbyId(server, fromClient, i, 0); + } + } + + ServerPackets.FinishedSendingLobbyIDs(server, fromClient); + } + + /// + /// Handle for if a client requests an ID from the Lobby server database matching a UUID + /// + /// What server the Client is in + /// What Client the request is from + /// The data + public static void RequestLobbyIdsWithMatchingAttribute(BaseServer.Server server, int fromClient, Packet packet) + { + var clientIdCheck = packet.ReadInt(); + var clientAttribName = packet.ReadString(); + var clientAttribValue = packet.ReadString(); + var tcp = server.Clients[fromClient].Tcp; + if (tcp is { Socket: { } }) + Terminal.LogInfo( + $"[{server.DisplayName}] {tcp.Socket.Client.RemoteEndPoint} requests all Lobby IDs with a matching Attribute ({clientAttribName}={clientAttribValue})"); + + if (fromClient != clientIdCheck) + { + Terminal.LogWarn( + $"[{server.DisplayName}] [RequestLobbyIDMatchingUUID] Player (ID: {fromClient}) has assumed the wrong client ID ({clientIdCheck})!"); + } + + var i = 1; + foreach (var lobby in Program.LobbyServ!.Clients) + { + if (lobby.Value.Attributes.GetAttribute(clientAttribName) == clientAttribValue) + { + Terminal.LogSuccess( + $"[{server.DisplayName}] Found Lobby with matching Attribute value ({clientAttribName}={clientAttribValue}). LobbyId: {i}"); + ServerPackets.SendLobbyId(server, fromClient, i, 0); + } + + + i++; + } + + ServerPackets.FinishedSendingLobbyIDs(server, fromClient); + } + + + public static void GetLobbyAttribute(BaseServer.Server server, int fromClient, Packet packet) + { + var clientIdCheck = packet.ReadInt(); + var requestedId = packet.ReadInt(); + var name = packet.ReadString(); + + var tcp = server.Clients[fromClient].Tcp; + if (tcp is { Socket: { } }) + Terminal.LogInfo( + $"[{server.DisplayName}] Client {fromClient} requests Lobby Attribute {name}."); + if (fromClient != clientIdCheck) + { + Terminal.LogWarn( + $"[{server.DisplayName}] GetLobbyAttribute] Player (ID: {fromClient}) has assumed the wrong client ID ({clientIdCheck})!"); + } + + if (!Program.LobbyServ!.Clients[requestedId].IsConnected) + { + Program.LobbyServ.Clients[requestedId].Disconnect(); + Terminal.LogDebug($"[{server.DisplayName}] Attempted to get Attribute of disconnected lobby!"); + ServerPackets.GetLobbyAttributesReceived(server, fromClient, requestedId, name, + "ERR_DISCONNECTED_LOBBY"); + return; + } + + var value = Program.LobbyServ.Clients[requestedId].Attributes.GetAttribute(name); + ServerPackets.GetLobbyAttributesReceived(server, fromClient, requestedId, name, value ?? ""); + } + } + + private static class ServerPackets + { + /// + /// Send a Server's ID through a Callback + /// + /// What server the Client is in + /// ID for the recipient + /// What ID is being passed from the Server database + public static void SendLobbyId(BaseServer.Server server, int toClient, int serverId, int mod) + { + Terminal.LogDebug($"[{server.DisplayName}] Sending lobby ID {serverId} to Client {toClient}..."); + using var packet = new Packet(10); + packet.Write(serverId); + packet.Write(mod); + + ServerSend.SendTcpData(server, toClient, packet); + } + + /// + /// Send a Server's Attribute to the Client + /// + /// What server the Client is in + /// ID for the recipient + /// What Server the Attribute is from + /// The Attribute name + /// The Attribute value + public static void GetLobbyAttributesReceived(BaseServer.Server server, int toClient, int requestedId, + string name, string value) + { + Terminal.LogDebug( + $"[{server.DisplayName}] Sending Lobby Attributes. toClient={toClient} requestedId={requestedId} name={name} value={value}"); + using var packet = new Packet(11); + packet.Write(requestedId); + packet.Write(name); + packet.Write(value); + + ServerSend.SendUdpData(server, toClient, packet); + } + + public static void FinishedSendingLobbyIDs(BaseServer.Server server, int toClient) + { + Terminal.LogDebug( + $"[{server.DisplayName}] Informing client that we have finished sending Lobby IDs..."); + using var packet = new Packet(12); + + ServerSend.SendTcpData(server, toClient, packet); + } + } +} \ No newline at end of file diff --git a/Matchmaker/ServerProgram.cs b/Matchmaker/ServerProgram.cs new file mode 100755 index 0000000..5d5b4a9 --- /dev/null +++ b/Matchmaker/ServerProgram.cs @@ -0,0 +1,67 @@ +using Matchmaker.Server.BaseServer; + +namespace Matchmaker.Server; + +public static class Program +{ + // ReSharper disable once FieldCanBeMadeReadOnly.Global + // ReSharper disable once MemberCanBePrivate.Global + // ReSharper disable once ConvertToConstant.Global + public static bool RunLoop = true; + + + private static BaseServer.Server? _mainServ = new(); + public static BaseServer.Server? LobbyServ = new(); + private static bool _serverStarted; + + public static void Main() + { + Console.WriteLine( + $"Techiesplash's Matchmaking API - Server (Ver. {Config.MatchmakerAPIVersion})"); + StartServer(); + + while (RunLoop) + { + } + } + + public static void StartServer() + { + if (!_serverStarted) + { + Console.Title = "Matchmaking Server"; + + ThreadManager.Start(); + + var ClientPackets = new BaseServer.Server.ServerPackets( + new Dictionary + { + { 10, Packets.PacketHandlers.RequestAllLobbyIDs }, + { 11, Packets.PacketHandlers.RequestLobbyIdsWithMatchingAttribute }, + { 12, Packets.PacketHandlers.GetLobbyAttribute } + }); + + var LobbyPackets = new BaseServer.Server.ServerPackets( + new Dictionary + { + }); + _mainServ = new BaseServer.Server(); + LobbyServ = new BaseServer.Server(); + _mainServ.Start(ClientPackets, Config.ListMaxClients, 26950, "List Server"); + LobbyServ.Start(LobbyPackets, Config.MaxLobbies, 26952, "Lobby Server"); + _serverStarted = true; + } + else + { + Terminal.LogWarn("Server already started."); + } + } + + public static void StopServer() + { + _serverStarted = false; + _mainServ = null; + LobbyServ = null; + ThreadManager.Stop(); + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..56de51d --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +
+ +# Matchmaker-API - Server + + ![Linux](https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black) + ![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white) + +![GitHub forks](https://img.shields.io/github/forks/Techiesplash/Matchmaker-API-Server) +![GitHub repo size](https://img.shields.io/github/repo-size/Techiesplash/Matchmaker-API-Server) +![GitHub all releases](https://img.shields.io/github/downloads/Techiesplash/Matchmaker-API-Server/total) +![GitHub issues](https://img.shields.io/github/issues/Techiesplash/Matchmaker-API-Server) + +![GitHub](https://img.shields.io/github/license/Techiesplash/Matchmaker-API-Server) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/Techiesplash/Matchmaker-API-Server) + + +

Introduction

+This is a project for implementing a Matchmaker API into Unity3D. +
+It can be expanded with custom packets as needed. +
+
+It depends on another project to be used in Unity. https://github.com/Techiesplash/Matchmaker-API-Client-Unity3d +

+This project is built upon MIT-Licensed code by Tom Weiland meant for a tutorial series. +Please check out his work: https://github.com/tom-weiland/tcp-udp-networking +
+ +![UVS Preview](./Images/preview.png) + +

Building

+You can open this in Visual Studio or Visual Studio Code and it should be ready to run immediately.
+To compile from the CLI, use 'dotnet build' or 'dotnet build --configuration Release'. +

+

Anyone is free to use, copy, modify, merge, publish, distribute, sublicense, or and/or sell copies of the software.