first commit

This commit is contained in:
techiesplash 2023-01-15 13:26:28 -08:00
commit 90b9eae405
43 changed files with 2245 additions and 0 deletions

2
.gitignore vendored Executable file
View file

@ -0,0 +1,2 @@
Matchmaker/bin/
Matchmaker/obj/

13
.idea/.idea.Matchmaker-API---Server.dir/.idea/.gitignore generated vendored Executable file
View file

@ -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

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>
</project>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

13
.idea/.idea.Matchmaker/.idea/.gitignore generated vendored Executable file
View file

@ -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

4
.idea/.idea.Matchmaker/.idea/encodings.xml generated Executable file
View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
</project>

8
.idea/.idea.Matchmaker/.idea/indexLayout.xml generated Executable file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>
</project>

6
.idea/.idea.Matchmaker/.idea/misc.xml generated Executable file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.jetbrains.rider.android.RiderAndroidMiscFileCreationComponent">
<option name="ENSURE_MISC_FILE_EXISTS" value="true" />
</component>
</project>

6
.idea/.idea.Matchmaker/.idea/vcs.xml generated Executable file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

Binary file not shown.

View file

BIN
.vs/Matchmaker/v17/.futdcache.v1 Executable file

Binary file not shown.

BIN
.vs/Matchmaker/v17/.suo Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

27
.vscode/launch.json vendored Executable file
View file

@ -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"
}
]
}

41
.vscode/tasks.json vendored Executable file
View file

@ -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"
}
]
}

BIN
Images/preview.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

22
LICENSE.md Executable file
View file

@ -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.

16
Matchmaker.sln Executable file
View file

@ -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

View file

@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dowling/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=NONINFRINGEMENT/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Techiesplash_0027s/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Weiland/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

26
Matchmaker/.vscode/launch.json vendored Executable file
View file

@ -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"
}
]
}

41
Matchmaker/.vscode/tasks.json vendored Executable file
View file

@ -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"
}
]
}

View file

@ -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<Attribute> _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();
}
}

306
Matchmaker/BaseServer/Client.cs Executable file
View file

@ -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<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(() =>
{
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();
}
}

View file

@ -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;
}

View file

@ -0,0 +1,12 @@
namespace Matchmaker.Server.BaseServer;
internal static class GameLogic
{
public static void Update()
{
ThreadManager.UpdateMain();
}
}

398
Matchmaker/BaseServer/Packet.cs Executable file
View file

@ -0,0 +1,398 @@
using System.Numerics;
using System.Text;
namespace Matchmaker.Server.BaseServer;
public sealed class Packet : IDisposable
{
private List<byte> _buffer;
private byte[] _readableBuffer = null!;
private int _readPos;
/// <summary>Creates a new empty packet (without an ID).</summary>
public Packet()
{
_buffer = new List<byte>(); // Initialize buffer
_readPos = 0; // Set readPos to 0
}
/// <summary>Creates a new packet with a given ID. Used for sending.</summary>
/// <param name="id">The packet ID.</param>
public Packet(int id)
{
_buffer = new List<byte>(); // Initialize buffer
_readPos = 0; // Set readPos to 0
Write(id); // Write packet id to the buffer
}
/// <summary>Creates a packet from which data can be read. Used for receiving.</summary>
/// <param name="data">The bytes to add to the packet.</param>
public Packet(IEnumerable<byte> data)
{
_buffer = new List<byte>(); // Initialize buffer
_readPos = 0; // Set readPos to 0
SetBytes(data);
}
#region Functions
/// <summary>Sets the packet's content and prepares it to be read.</summary>
/// <param name="data">The bytes to add to the packet.</param>
public void SetBytes(IEnumerable<byte> data)
{
Write(data);
_readableBuffer = _buffer.ToArray();
}
/// <summary>Inserts the length of the packet's content at the start of the buffer.</summary>
public void WriteLength()
{
_buffer.InsertRange(0, BitConverter.GetBytes(_buffer.Count)); // Insert the byte length of the packet at the very beginning
}
/// <summary>Inserts the given int at the start of the buffer.</summary>
/// <param name="value">The int to insert.</param>
// 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
}
/// <summary>Gets the packet's content in array form.</summary>
public byte[] ToArray()
{
_readableBuffer = _buffer.ToArray();
return _readableBuffer;
}
/// <summary>Gets the length of the packet's content.</summary>
public int Length()
{
return _buffer.Count; // Return the length of buffer
}
/// <summary>Gets the length of the unread data contained in the packet.</summary>
public int UnreadLength()
{
return Length() - _readPos; // Return the remaining length (unread)
}
/// <summary>Resets the packet instance to allow it to be reused.</summary>
/// <param name="shouldReset">Whether or not to reset the packet.</param>
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
/// <summary>Adds a byte to the packet.</summary>
/// <param name="value">The byte to add.</param>
// ReSharper disable once UnusedMember.Global
public void Write(byte value)
{
_buffer.Add(value);
}
/// <summary>Adds an array of bytes to the packet.</summary>
/// <param name="value">The byte array to add.</param>
// ReSharper disable once MemberCanBePrivate.Global
public void Write(IEnumerable<byte> value)
{
_buffer.AddRange(value);
}
/// <summary>Adds a short to the packet.</summary>
/// <param name="value">The short to add.</param>
// ReSharper disable once UnusedMember.Global
public void Write(short value)
{
_buffer.AddRange(BitConverter.GetBytes(value));
}
/// <summary>Adds an int to the packet.</summary>
/// <param name="value">The int to add.</param>
public void Write(int value)
{
_buffer.AddRange(BitConverter.GetBytes(value));
}
/// <summary>Adds a long to the packet.</summary>
/// <param name="value">The long to add.</param>
// ReSharper disable once UnusedMember.Global
public void Write(long value)
{
_buffer.AddRange(BitConverter.GetBytes(value));
}
/// <summary>Adds a float to the packet.</summary>
/// <param name="value">The float to add.</param>
// ReSharper disable once UnusedMember.Global
public void Write(float value)
{
_buffer.AddRange(BitConverter.GetBytes(value));
}
/// <summary>Adds a bool to the packet.</summary>
/// <param name="value">The bool to add.</param>
// ReSharper disable once UnusedMember.Global
public void Write(bool value)
{
_buffer.AddRange(BitConverter.GetBytes(value));
}
/// <summary>Adds a string to the packet.</summary>
/// <param name="value">The string to add.</param>
// 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
}
/// <summary>Adds a Vector3 to the packet.</summary>
/// <param name="value">The Vector3 to add.</param>
// ReSharper disable once UnusedMember.Global
public void Write(Vector3 value)
{
Write(value.X);
Write(value.Y);
Write(value.Z);
}
/// <summary>Adds a Quaternion to the packet.</summary>
/// <param name="value">The Quaternion to add.</param>
// 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
/// <summary>Reads a byte from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
// 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'!");
}
}
/// <summary>Reads an array of bytes from the packet.</summary>
/// <param name="length">The length of the byte array.</param>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
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[]'!");
}
}
/// <summary>Reads a short from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
// 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'!");
}
}
/// <summary>Reads an int from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
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'!");
}
}
/// <summary>Reads a long from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
// 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'!");
}
}
/// <summary>Reads a float from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
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'!");
}
}
/// <summary>Reads a bool from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
// 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'!");
}
}
/// <summary>Reads a string from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
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'!");
}
}
/// <summary>Reads a Vector3 from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
// ReSharper disable once UnusedMember.Global
public Vector3 ReadVector3(bool moveReadPos = true)
{
return new Vector3(ReadFloat(moveReadPos), ReadFloat(moveReadPos), ReadFloat(moveReadPos));
}
/// <summary>Reads a Quaternion from the packet.</summary>
/// <param name="moveReadPos">Whether or not to move the buffer's read position.</param>
// 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);
}
}

159
Matchmaker/BaseServer/Server.cs Executable file
View file

@ -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<int, Client> 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<int, PacketHandler> PacketHandlers;
public ServerPackets(Dictionary<int, PacketHandler> 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<int, Delegate> packetHandlers = new Dictionary<int, Delegate>()
{
{ (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.");
}
}

View file

@ -0,0 +1,156 @@
using System.Net;
// ReSharper disable once CheckNamespace
namespace Matchmaker.Server.BaseServer;
internal static class ServerHandle
{
/// <summary>
/// Gets the message received in response to the Welcome message
/// </summary>
/// <param name="server"></param>
/// <param name="fromClient"></param>
/// <param name="packet"></param>
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<string>();
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");
}
}
}

View file

@ -0,0 +1,277 @@
namespace Matchmaker.Server.BaseServer;
public static class ServerSend
{
public enum StatusType
{
OK,
RECEIVED,
FAIL
}
/// <summary>
/// Sends TCP data to a Client
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="packet">Data to send</param>
public static void SendTcpData(Server server, int toClient, Packet packet)
{
packet.WriteLength();
var tcp = server.Clients[toClient].Tcp;
tcp?.SendData(packet);
}
/// <summary>
/// Sends UDP data to a Client
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="packet">Data to send</param>
// 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);
}
/// <summary>
/// Sends TCP data to all Clients
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="packet">Data to send</param>
// 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);
}
}
/// <summary>
/// Sends TCP data to all Clients except one
/// </summary>
/// <param name="server">Server the Client is in</param>
/// /// <param name="exceptClient">What Client to exclude</param>
/// <param name="packet">Data to send</param>
// 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);
}
}
/// <summary>
/// Sends UDP data to all Clients
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="packet">Data to send</param>
// 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);
}
}
/// <summary>
/// Sends UDP data to all Clients except one
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="exceptClient">What Client to exclude</param>
/// <param name="packet">Data to send</param>
// 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);
}
}
/// <summary>
/// Sends TCP data to a Client
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="packet">Data to send</param>
public static void SendTcpDataNoSync(Server server, int toClient, Packet packet)
{
packet.WriteLength();
var tcp = server.Clients[toClient].Tcp;
tcp?.SendData(packet);
}
/// <summary>
/// Sends UDP data to a Client
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="packet">Data to send</param>
// 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);
}
/// <summary>
/// Sends TCP data to all Clients
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="packet">Data to send</param>
// 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);
}
}
/// <summary>
/// Sends TCP data to all Clients except one
/// </summary>
/// <param name="server">Server the Client is in</param>
/// /// <param name="exceptClient">What Client to exclude</param>
/// <param name="packet">Data to send</param>
// 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);
}
}
/// <summary>
/// Sends UDP data to all Clients
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="packet">Data to send</param>
// 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);
}
}
/// <summary>
/// Sends UDP data to all Clients except one
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="exceptClient">What Client to exclude</param>
/// <param name="packet">Data to send</param>
// 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
/// <summary>
/// Welcome message for Client
/// </summary>
/// <param name="server">Server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="msg">Message to send</param>
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);
}
/// <summary>
/// Disconnect a Client
/// </summary>
/// <param name="server">What server the Client is in</param>
/// <param name="toClient">What Client to kick</param>
public static void DisconnectClient(Server server, int toClient)
{
using var packet = new Packet(int.MaxValue-1);
SendTcpData(server, toClient, packet);
}
/// <summary>
/// Send an Attribute to the Client
/// </summary>
/// <param name="server">What server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="requestedId">What Client the Attribute is from</param>
/// <param name="name">The Attribute name</param>
/// <param name="value">The Attribute value</param>
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);
}
/// <summary>
/// Respond to the client with a status
/// </summary>
/// <param name="server">What server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="status">What status</param>
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
}

View file

@ -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;
}
}

View file

@ -0,0 +1,93 @@
namespace Matchmaker.Server.BaseServer;
internal static class ThreadManager
{
private static readonly List<Action> ExecuteOnMainThreadList = new();
private static readonly List<Action> ExecuteCopiedOnMainThread = new();
private static bool _actionToExecuteOnMainThread;
/// <summary>
/// Controls whether or not the Main Thread should run. Set to FALSE to shut down Main Thread.
/// </summary>
private static bool _isRunning;
private static Thread? _thread;
/// <summary>Sets an action to be executed on the main thread.</summary>
/// <param name="action">The action to be executed on the main thread.</param>
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;
}
}
/// <summary>
/// Start the Main Thread so Packets can be acted upon
/// </summary>
public static void Start()
{
_isRunning = true;
_thread = new Thread(MainThread);
_thread.Start();
}
/// <summary>
/// Stop the Main Thread
/// </summary>
public static void Stop()
{
_isRunning = false;
}
/// <summary>
/// Executes all code meant to run on the main thread. NOTE: Call this ONLY from the main thread.
/// </summary>
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);
}
}
}
}
}

69
Matchmaker/BaseServer/UUID.cs Executable file
View file

@ -0,0 +1,69 @@
namespace Matchmaker.Server.BaseServer;
/// <summary>
/// A UUID Object
/// </summary>
public class UUID
{
private string value = "";
/// <summary>
/// Generates a random string of specified length
/// </summary>
/// <param name="length">How long a random string to make</param>
/// <returns>A random string</returns>
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;
}
/// <summary>
/// Get the UUID value
/// </summary>
/// <returns>The UUID's value</returns>
public string GetValue()
{
return value;
}
public UUID(int length, List<string> 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;
}
}

90
Matchmaker/Config.cs Executable file
View file

@ -0,0 +1,90 @@
namespace Matchmaker.Server;
public static class Config
{
/// <summary>
/// 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.
/// </summary>
public const string GameId = "My Game Name";
/// <summary>
/// The version of the matchmaker API. This must match here and on the client for a connection.
/// </summary>
public const string MatchmakerAPIVersion = "1.1.6";
/// <summary>
/// What version the game is. This must match here and on the client for a connection.
/// </summary>
public const string GameVersion = "0.0.1";
/// <summary>
/// How many people can be connected to the List server instance at the same time.
/// </summary>
public const ushort ListMaxClients = 512;
/// <summary>
/// The maximum amount of Lobbies.
/// </summary>
public const ushort MaxLobbies = 1024;
/// <summary>
/// What port the server will be on (0-65535). The lobby server will use a port two (2) above this.
/// </summary>
public const ushort Port = 26950;
/// <summary>
/// The display name for the List server instance, if you want to change it.
/// </summary>
public const string ListServerDisplayName = "List Server";
/// <summary>
/// The display name for the Lobby server instance, if you want to change it.
/// </summary>
public const string LobbyServerDisplayName = "Lobby Server";
/// <summary>
/// How long generated UUIDs are
/// </summary>
public const int UuidLength = 7;
/// <summary>
/// Sends a response to the Client whenever a message is received. Slow, for debugging.
/// </summary>
public const bool Sync = false;
/// <summary>
/// Whether or not to show LogDebug() text.
/// </summary>
public const bool ShowTerminalDebug = true;
/// <summary>
/// Text that is prepended to a Log() text.
/// </summary>
public const string LogText = " LOG ";
/// <summary>
/// Text that is prepended to a LogDebug() text.
/// </summary>
public const string LogDebugText = " DEBUG ";
/// <summary>
/// Text that is prepended to a LogInfo() text.
/// </summary>
public const string LogInfoText = " CHECK ";
/// <summary>
/// Text that is prepended to a LogWarn() text.
/// </summary>
public const string LogWarningText = "WARNING ";
/// <summary>
/// Text that is prepended to a LogError() text.
/// </summary>
public const string LogErrorText = " ERROR ";
/// <summary>
/// Text that is prepended to a LogSuccess() text.
/// </summary>
public const string LogSuccessText = "SUCCESS ";
}

10
Matchmaker/Matchmaker.csproj Executable file
View file

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

159
Matchmaker/ServerPackets.cs Executable file
View file

@ -0,0 +1,159 @@
using Matchmaker.Server.BaseServer;
namespace Matchmaker.Server;
public static class Packets
{
public static class PacketHandlers
{
/// <summary>
/// Handle for if a client requests all IDs in the Lobby server database
/// </summary>
/// <param name="server">What server the Client is in</param>
/// <param name="fromClient">What Client the request is from</param>
/// <param name="packet">The data</param>
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);
}
/// <summary>
/// Handle for if a client requests an ID from the Lobby server database matching a UUID
/// </summary>
/// <param name="server">What server the Client is in</param>
/// <param name="fromClient">What Client the request is from</param>
/// <param name="packet">The data</param>
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
{
/// <summary>
/// Send a Server's ID through a Callback
/// </summary>
/// <param name="server">What server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="serverId">What ID is being passed from the Server database</param>
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);
}
/// <summary>
/// Send a Server's Attribute to the Client
/// </summary>
/// <param name="server">What server the Client is in</param>
/// <param name="toClient">ID for the recipient</param>
/// <param name="requestedId">What Server the Attribute is from</param>
/// <param name="name">The Attribute name</param>
/// <param name="value">The Attribute value</param>
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);
}
}
}

67
Matchmaker/ServerProgram.cs Executable file
View file

@ -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<int, BaseServer.Server.ServerPackets.PacketHandler>
{
{ 10, Packets.PacketHandlers.RequestAllLobbyIDs },
{ 11, Packets.PacketHandlers.RequestLobbyIdsWithMatchingAttribute },
{ 12, Packets.PacketHandlers.GetLobbyAttribute }
});
var LobbyPackets = new BaseServer.Server.ServerPackets(
new Dictionary<int, BaseServer.Server.ServerPackets.PacketHandler>
{
});
_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();
}
}

35
README.md Executable file
View file

@ -0,0 +1,35 @@
<div align="center">
# 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)
<h2>Introduction</h2>
This is a project for implementing a Matchmaker API into Unity3D.
<br />
It can be expanded with custom packets as needed.
<br />
<br />
It depends on another project to be used in Unity. https://github.com/Techiesplash/Matchmaker-API-Client-Unity3d
<br /><br />
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
<br />
![UVS Preview](./Images/preview.png)
<h2>Building</h2>
You can open this in Visual Studio or Visual Studio Code and it should be ready to run immediately.<br />
To compile from the CLI, use 'dotnet build' or 'dotnet build --configuration Release'.
<br /><br />
<h3>Anyone is free to use, copy, modify, merge, publish, distribute, sublicense, or and/or sell copies of the software.</h3>