baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
// <copyright file="CheckMaximumConnectionsPlugin.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// Plugin which checks if the maximum number of connections got exceeded. Refuses the connection to new clients, if that happens.
/// </summary>
internal class CheckMaximumConnectionsPlugin : IAfterSocketAcceptPlugin
{
private readonly ILogger<CheckMaximumConnectionsPlugin> _logger;
private readonly ClientListener _clientListener;
private readonly IConnectServerSettings _connectServerSettings;
/// <summary>
/// Initializes a new instance of the <see cref="CheckMaximumConnectionsPlugin" /> class.
/// </summary>
/// <param name="server">The server.</param>
/// <param name="logger">The logger.</param>
public CheckMaximumConnectionsPlugin(ConnectServer server, ILogger<CheckMaximumConnectionsPlugin> logger)
{
this._logger = logger;
this._clientListener = server.ClientListener;
this._connectServerSettings = server.Settings;
}
/// <inheritdoc/>
public bool OnAfterSocketAccept(Socket socket)
{
var maxConnections = this._connectServerSettings.MaxConnections;
if (maxConnections <= this._clientListener.Clients.Count)
{
this._logger.LogWarning("Connection refused from {0}: maximum connections ({1}) reached.", socket.RemoteEndPoint, maxConnections);
return false;
}
return true;
}
}

138
src/ConnectServer/Client.cs Normal file
View File

@@ -0,0 +1,138 @@
// <copyright file="Client.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Buffers;
using System.Net;
using System.Threading;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.ConnectServer.PacketHandler;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ConnectServer;
using Nito.AsyncEx.Synchronous;
/// <summary>
/// The client which connected to the connect server.
/// </summary>
internal sealed class Client : IDisposable
{
private readonly ILogger<Client> _logger;
private readonly byte[] _receiveBuffer;
private readonly Timer _onlineTimer;
private readonly IPacketHandler<Client> _packetHandler;
private bool _disposed;
private DateTime _lastReceive;
/// <summary>
/// Initializes a new instance of the <see cref="Client" /> class.
/// </summary>
/// <param name="connection">The connection.</param>
/// <param name="timeout">The timeout.</param>
/// <param name="packetHandler">The packet handler.</param>
/// <param name="maxPacketSize">Maximum size of the packet. This value is also used to initialize the receive buffer.</param>
/// <param name="logger">The logger.</param>
public Client(IConnection connection, TimeSpan timeout, IPacketHandler<Client> packetHandler, byte maxPacketSize, ILogger<Client> logger)
{
this.Connection = connection;
this.Connection.PacketReceived += this.OnPacketReceivedAsync;
this.Timeout = timeout;
this._packetHandler = packetHandler;
this._logger = logger;
this._lastReceive = DateTime.Now;
var checkInterval = new TimeSpan(0, 0, 20);
this._onlineTimer = new Timer(this.OnOnlineTimerElapsed, null, checkInterval, checkInterval);
this._receiveBuffer = new byte[maxPacketSize];
}
/// <summary>
/// Gets or sets the timeout after which the client gets disconnected if he is inactive.
/// </summary>
public TimeSpan Timeout { get; set; }
/// <summary>
/// Gets or sets the server information request count.
/// </summary>
/// <remarks>Used for DOS protection.</remarks>
public int ServerInfoRequestCount { get; set; }
/// <summary>
/// Gets or sets the FTP request count.
/// </summary>
/// <remarks>Used for DOS protection.</remarks>
public int FtpRequestCount { get; set; }
/// <summary>
/// Gets or sets the server list request count.
/// </summary>
/// <remarks>Used for DOS protection.</remarks>
public int ServerListRequestCount { get; set; }
/// <summary>
/// Gets or sets the ip from which the client is connecting.
/// </summary>
public IPAddress Address { get; set; } = IPAddress.None;
/// <summary>
/// Gets or sets the port from which the client is connecting.
/// </summary>
public int Port { get; set; }
/// <summary>
/// Gets the connection from/to the client.
/// </summary>
internal IConnection Connection { get; }
/// <inheritdoc/>
public void Dispose()
{
if (!this._disposed)
{
this._disposed = true;
this._onlineTimer.Dispose();
this.Connection.Dispose();
}
}
/// <summary>
/// Sends the hello packet.
/// </summary>
internal ValueTask SendHelloAsync()
{
return this.Connection.SendHelloAsync();
}
private void OnOnlineTimerElapsed(object? state)
{
try
{
if (this.Connection.Connected && DateTime.Now.Subtract(this._lastReceive) > this.Timeout)
{
this._logger.LogDebug("Connection Timeout ({0}): Address {1}:{2} will be disconnected.", this.Timeout, this.Address, this.Port);
this.Connection.DisconnectAsync().AsTask().WaitAndUnwrapException();
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error when disconnecting client. Address {1}:{2}", this.Address, this.Port);
}
}
private async ValueTask OnPacketReceivedAsync(ReadOnlySequence<byte> sequence)
{
this._lastReceive = DateTime.Now;
if (sequence.Length > this._receiveBuffer.Length)
{
this._logger.LogInformation($"Client {this.Address}:{this.Port} will be disconnected because it sent a packet which was too big (size of {sequence.Length}");
await this.Connection.DisconnectAsync().ConfigureAwait(false);
}
sequence.CopyTo(this._receiveBuffer);
await this._packetHandler
.HandlePacketAsync(this, this._receiveBuffer.AsMemory(0, this._receiveBuffer.GetPacketSize()))
.ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,60 @@
// <copyright file="ClientConnectionCountPlugin.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Net;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// The client connection count plugin.
/// </summary>
internal class ClientConnectionCountPlugin : IAfterSocketAcceptPlugin, IAfterDisconnectPlugin
{
private readonly ILogger<ClientConnectionCountPlugin> _logger;
private readonly ClientConnectionCounter _clientCounter;
private readonly IConnectServerSettings _connectServerSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ClientConnectionCountPlugin" /> class.
/// </summary>
/// <param name="connectServerSettings">The settings.</param>
/// <param name="logger">The logger.</param>
public ClientConnectionCountPlugin(IConnectServerSettings connectServerSettings, ILogger<ClientConnectionCountPlugin> logger)
{
this._connectServerSettings = connectServerSettings;
this._logger = logger;
this._clientCounter = new ClientConnectionCounter();
}
/// <inheritdoc/>
public bool OnAfterSocketAccept(Socket socket)
{
var ipAddress = (socket.RemoteEndPoint as IPEndPoint)?.Address;
if (ipAddress is null)
{
// should never happen - but who knows. In this case, we allow the connection.
this._logger.LogDebug($"Non-IPEndPoint connected: {socket.RemoteEndPoint}.");
return true;
}
if (this._connectServerSettings.CheckMaxConnectionsPerAddress
&& this._clientCounter.GetConnectionCount(ipAddress) >= this._connectServerSettings.MaxConnectionsPerAddress)
{
this._logger.LogWarning("Maximum Connections per IP reached: {0}, Connection refused.", ipAddress);
return false;
}
this._clientCounter.AddConnection(ipAddress);
return true;
}
/// <inheritdoc/>
public void OnAfterDisconnect(Client client)
{
this._clientCounter.RemoveConnection(client.Address);
}
}

View File

@@ -0,0 +1,72 @@
// <copyright file="ClientConnectionCounter.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Net;
/// <summary>
/// Counts the connections per ip address.
/// </summary>
internal class ClientConnectionCounter
{
private readonly IDictionary<IPAddress, int> _connections = new Dictionary<IPAddress, int>();
private readonly object _syncRoot = new();
/// <summary>
/// Gets the connection count of the specified ip address.
/// </summary>
/// <param name="ipAddress">The ip address.</param>
/// <returns>The counted connections of the ip address.</returns>
public int GetConnectionCount(IPAddress ipAddress)
{
int count;
lock (this._syncRoot)
{
this._connections.TryGetValue(ipAddress, out count);
}
return count;
}
/// <summary>
/// Adds the connection and increases its count for the specified ip address.
/// </summary>
/// <param name="ipAddress">The ip address.</param>
public void AddConnection(IPAddress ipAddress)
{
lock (this._syncRoot)
{
if (this._connections.ContainsKey(ipAddress))
{
this._connections[ipAddress]++;
}
else
{
this._connections.Add(ipAddress, 1);
}
}
}
/// <summary>
/// Removes the connection and decreases its count for the specified ip address.
/// </summary>
/// <param name="ipAddress">The ip address.</param>
public void RemoveConnection(IPAddress ipAddress)
{
lock (this._syncRoot)
{
if (!this._connections.ContainsKey(ipAddress))
{
return;
}
this._connections[ipAddress]--;
if (this._connections[ipAddress] == 0)
{
this._connections.Remove(ipAddress);
}
}
}
}

View File

@@ -0,0 +1,133 @@
// <copyright file="ClientListener.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Net;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.ConnectServer.PacketHandler;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Network;
using Nito.AsyncEx;
/// <summary>
/// The listener which is waiting for new connecting clients.
/// </summary>
internal class ClientListener
{
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger<ClientListener> _logger;
private readonly AsyncLock _clientListLock = new();
private readonly IConnectServerSettings _connectServerSettings;
private readonly IPacketHandler<Client> _packetHandler;
private Listener? _listener;
/// <summary>
/// Initializes a new instance of the <see cref="ClientListener" /> class.
/// </summary>
/// <param name="connectServer">The connect server.</param>
/// <param name="loggerFactory">The logger factory.</param>
public ClientListener(IConnectServer connectServer, ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
this._connectServerSettings = connectServer.Settings;
this._logger = this._loggerFactory.CreateLogger<ClientListener>();
this._packetHandler = new ClientPacketHandler(connectServer, loggerFactory);
this.Clients = new List<Client>();
this.ClientSocketAcceptPlugins = new List<IAfterSocketAcceptPlugin>();
this.ClientSocketDisconnectPlugins = new List<IAfterDisconnectPlugin>();
}
/// <summary>
/// Occurs when the number of connected clients changed.
/// </summary>
public event EventHandler? ConnectedClientsChanged;
/// <summary>
/// Gets the connected clients.
/// </summary>
public ICollection<Client> Clients { get; }
/// <summary>
/// Gets the client socket accept plugins.
/// </summary>
public IList<IAfterSocketAcceptPlugin> ClientSocketAcceptPlugins { get; }
/// <summary>
/// Gets the client socket disconnect plugins.
/// </summary>
public ICollection<IAfterDisconnectPlugin> ClientSocketDisconnectPlugins { get; }
/// <summary>
/// Starts the listener.
/// </summary>
public void StartListener()
{
this._listener = new Listener(this._connectServerSettings.ClientListenerPort, null, null, this._loggerFactory);
this._listener.ClientAccepting += this.OnClientAcceptingAsync;
this._listener.ClientAccepted += this.OnClientAcceptedAsync;
this._listener.Start(this._connectServerSettings.ListenerBacklog);
this._logger.LogInformation("Client Listener started, Port {0}", this._connectServerSettings.ClientListenerPort);
}
/// <summary>
/// Stops the listener.
/// </summary>
public void StopListener()
{
this._listener?.Stop();
this._logger.LogInformation("Client Listener stopped");
}
private async ValueTask OnClientAcceptingAsync(ClientAcceptingEventArgs e)
{
for (var i = 0; i < this.ClientSocketAcceptPlugins.Count; ++i)
{
var plugin = this.ClientSocketAcceptPlugins[i];
if (!plugin.OnAfterSocketAccept(e.AcceptingSocket))
{
e.Cancel = true;
break;
}
}
}
private async ValueTask OnClientAcceptedAsync(ClientAcceptedEventArgs e)
{
var connection = e.AcceptedConnection;
var client = new Client(connection, this._connectServerSettings.Timeout, this._packetHandler, this._connectServerSettings.MaximumReceiveSize, this._loggerFactory.CreateLogger<Client>());
var ipEndpoint = connection.EndPoint as IPEndPoint;
client.Address = ipEndpoint?.Address ?? IPAddress.None;
client.Port = ipEndpoint?.Port ?? 0;
client.Timeout = this._connectServerSettings.Timeout;
using (await this._clientListLock.LockAsync().ConfigureAwait(false))
{
this.Clients.Add(client);
}
client.Connection.Disconnected += async () => await this.OnClientDisconnectAsync(client).ConfigureAwait(false);
this._logger.LogDebug("Client connected: {0}, current client count: {1}", connection.EndPoint, this.Clients.Count);
await client.SendHelloAsync().ConfigureAwait(false);
_ = Task.Run(() => client.Connection.BeginReceiveAsync());
this.ConnectedClientsChanged?.Invoke(this, EventArgs.Empty);
}
private async ValueTask OnClientDisconnectAsync(Client client)
{
foreach (var plugin in this.ClientSocketDisconnectPlugins)
{
plugin.OnAfterDisconnect(client);
}
this._logger.LogDebug("Connection to Client {0}:{1} disconnected.", client.Address, client.Port);
using (await this._clientListLock.LockAsync().ConfigureAwait(false))
{
this.Clients.Remove(client);
}
this.ConnectedClientsChanged?.Invoke(this, EventArgs.Empty);
}
}

View File

@@ -0,0 +1,242 @@
// <copyright file="ConnectServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Network.PlugIns;
/// <summary>
/// The connect server.
/// </summary>
public class ConnectServer : IConnectServer, OpenMU.Interfaces.IConnectServer
{
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
private readonly ServerList _serverList;
private ServerState _serverState;
/// <summary>
/// Initializes a new instance of the <see cref="ConnectServer" /> class.
/// </summary>
/// <param name="connectServerSettings">The settings.</param>
/// <param name="loggerFactory">The logger factory.</param>
public ConnectServer(IConnectServerSettings connectServerSettings, ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
this.Settings = connectServerSettings;
this.ClientVersion = new ClientVersion(this.Settings.Client.Season, this.Settings.Client.Episode, ClientLanguage.Invariant);
this.ConfigurationId = this.Settings.ConfigurationId;
this._logger = this._loggerFactory.CreateLogger<ConnectServer>();
this.ConnectInfos = new ConcurrentDictionary<ushort, byte[]>();
this._serverList = new ServerList(this.ClientVersion);
this.ClientListener = new ClientListener(this, loggerFactory);
this.ClientListener.ConnectedClientsChanged += (_, _) =>
{
this.RaisePropertyChanged(nameof(this.CurrentConnections));
};
this.CreatePlugins();
}
/// <inheritdoc />
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc/>
public ServerState ServerState
{
get => this._serverState;
private set
{
if (value != this._serverState)
{
this._serverState = value;
this.RaisePropertyChanged();
}
}
}
/// <inheritdoc />
public ServerType Type => ServerType.ConnectServer;
/// <inheritdoc/>
public string Description => this.Settings.Description;
/// <inheritdoc/>
public int Id => SpecialServerIds.ConnectServer + this.Settings.ServerId;
/// <inheritdoc />
public Guid ConfigurationId { get; }
/// <inheritdoc/>
public ConcurrentDictionary<ushort, byte[]> ConnectInfos { get; }
/// <inheritdoc/>
ServerList IConnectServer.ServerList => this._serverList;
/// <inheritdoc cref="IConnectServer"/>
public IConnectServerSettings Settings { get; }
/// <inheritdoc />
public ClientVersion ClientVersion { get; }
/// <summary>
/// Gets the maximum allowed connections.
/// </summary>
public int MaximumConnections => this.Settings.MaxConnections;
/// <summary>
/// Gets the current connection count.
/// </summary>
public int CurrentConnections => this.ClientListener.Clients.Count;
/// <summary>
/// Gets the current game server connection count.
/// </summary>
public int CurrentGameServerConnections => this._serverList.TotalConnectionCount;
/// <summary>
/// Gets the registered game servers.
/// </summary>
public IEnumerable<IGameServerEntry> RegisteredGameServers => this._serverList.Items;
/// <summary>
/// Gets the client listener.
/// </summary>
internal ClientListener ClientListener { get; }
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
await this.StartAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask StartAsync()
{
if (this.ServerState != ServerState.Stopped)
{
return;
}
this._logger.LogInformation("Begin starting");
var oldState = this.ServerState;
this.ServerState = OpenMU.Interfaces.ServerState.Starting;
try
{
this.ClientListener.StartListener();
this.ServerState = OpenMU.Interfaces.ServerState.Started;
}
catch (Exception ex)
{
this._logger.LogError(ex, ex.Message);
this.ServerState = oldState;
}
this._logger.LogInformation("Finished starting");
}
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
await this.ShutdownAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public async ValueTask ShutdownAsync()
{
this._logger.LogInformation("Begin stopping");
this.ServerState = OpenMU.Interfaces.ServerState.Stopping;
this.ClientListener.StopListener();
this.ServerState = OpenMU.Interfaces.ServerState.Stopped;
this._logger.LogInformation("Finished stopping");
}
/// <inheritdoc/>
public void RegisterGameServer(ServerInfo gameServer, IPEndPoint publicEndPoint)
{
this._logger.LogInformation("GameServer {0} is registering with endpoint {1}", gameServer, publicEndPoint);
try
{
if (this.ConnectInfos.ContainsKey(gameServer.Id))
{
this._logger.LogInformation("GameServer {0} was already registered and needs to be removed before...", gameServer);
this.UnregisterGameServer(gameServer.Id);
}
var serverListItem = new ServerListItem(this._serverList)
{
ServerId = gameServer.Id,
EndPoint = publicEndPoint,
MaximumConnections = gameServer.MaximumConnections,
CurrentConnections = gameServer.CurrentConnections,
};
if (this.ConnectInfos.TryAdd(serverListItem.ServerId, serverListItem.ConnectInfo))
{
this._serverList.Add(serverListItem);
}
}
catch (Exception ex)
{
this._logger.LogError(ex, "Error during registration process");
throw;
}
this._logger.LogInformation("GameServer {0} has registered with endpoint {1}", gameServer, publicEndPoint);
}
/// <inheritdoc/>
public void UnregisterGameServer(ushort gameServerId)
{
this._logger.LogInformation("GameServer {0} is unregistering", gameServerId);
var serverListItem = this._serverList.GetItem(gameServerId);
if (serverListItem != null)
{
this.ConnectInfos.Remove(serverListItem.ServerId, out _);
this._serverList.Remove(serverListItem);
}
this._logger.LogInformation("GameServer {0} has unregistered", gameServerId);
}
/// <inheritdoc />
public void CurrentConnectionsChanged(ushort serverId, int currentConnections)
{
var serverListItem = this._serverList.GetItem(serverId);
if (serverListItem is null)
{
return;
}
serverListItem.CurrentConnections = currentConnections;
}
private void CreatePlugins()
{
this._logger.LogDebug("Begin creating plugins");
this.ClientListener.ClientSocketAcceptPlugins.Add(new CheckMaximumConnectionsPlugin(this, this._loggerFactory.CreateLogger<CheckMaximumConnectionsPlugin>()));
var clientCountPlugin = new ClientConnectionCountPlugin(this.Settings, this._loggerFactory.CreateLogger<ClientConnectionCountPlugin>());
this.ClientListener.ClientSocketAcceptPlugins.Add(clientCountPlugin);
this.ClientListener.ClientSocketDisconnectPlugins.Add(clientCountPlugin);
this._logger.LogDebug("Finished creating plugins");
}
/// <summary>
/// Called when a property changed.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
private void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

View File

@@ -0,0 +1,37 @@
// <copyright file="ConnectServerFactory.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
/// <summary>
/// The connect server factory.
/// </summary>
public class ConnectServerFactory
{
private readonly ILoggerFactory _loggerFactory;
/// <summary>
/// Initializes a new instance of the <see cref="ConnectServerFactory"/> class.
/// </summary>
/// <param name="loggerFactory">The logger factory.</param>
public ConnectServerFactory(ILoggerFactory loggerFactory)
{
this._loggerFactory = loggerFactory;
}
/// <summary>
/// Creates a new connect server instance.
/// </summary>
/// <param name="settings">The settings.</param>
/// <returns>
/// The new connect server instance.
/// </returns>
public OpenMU.Interfaces.IConnectServer CreateConnectServer(IConnectServerSettings settings)
{
return new ConnectServer(settings, this._loggerFactory);
}
}

View File

@@ -0,0 +1,17 @@
// <copyright file="IAfterDisconnectPlugin.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
/// <summary>
/// Plugin which is executed after a client disconnected.
/// </summary>
internal interface IAfterDisconnectPlugin
{
/// <summary>
/// Called after a client disconnected.
/// </summary>
/// <param name="client">The client.</param>
void OnAfterDisconnect(Client client);
}

View File

@@ -0,0 +1,20 @@
// <copyright file="IAfterSocketAcceptPlugin.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Net.Sockets;
/// <summary>
/// Plugin which is executed when a client socket got accepted by the listener.
/// </summary>
internal interface IAfterSocketAcceptPlugin
{
/// <summary>
/// Called after the client socket got accepted by the listener.
/// </summary>
/// <param name="socket">The socket.</param>
/// <returns>Flag that indicates if the socket is allowed to connect.</returns>
bool OnAfterSocketAccept(Socket socket);
}

View File

@@ -0,0 +1,35 @@
// <copyright file="IConnectServer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Collections.Concurrent;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Network.PlugIns;
/// <summary>
/// The internal interface of a connect server.
/// </summary>
internal interface IConnectServer
{
/// <summary>
/// Gets the connect infos.
/// </summary>
ConcurrentDictionary<ushort, byte[]> ConnectInfos { get; }
/// <summary>
/// Gets the server list.
/// </summary>
ServerList ServerList { get; }
/// <summary>
/// Gets the connectServerSettings.
/// </summary>
IConnectServerSettings Settings { get; }
/// <summary>
/// Gets the client version.
/// </summary>
ClientVersion ClientVersion { get; }
}

View File

@@ -0,0 +1,33 @@
// <copyright file="IGameServerEntry.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Net;
/// <summary>
/// Interface for an entry of a gameserver in the connect server.
/// </summary>
public interface IGameServerEntry
{
/// <summary>
/// Gets the server identifier.
/// </summary>
ushort ServerId { get; }
/// <summary>
/// Gets the end point under which the server is accessible.
/// </summary>
IPEndPoint EndPoint { get; }
/// <summary>
/// Gets the server load percentage.
/// </summary>
byte ServerLoadPercentage { get; }
/// <summary>
/// Gets the count of current connections.
/// </summary>
int CurrentConnections { get; }
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\bin\Debug\</OutputPath>
<DocumentationFile>..\..\bin\Debug\MUnique.OpenMU.ConnectServer.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\bin\Release\</OutputPath>
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.ConnectServer.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Interfaces\MUnique.OpenMU.Interfaces.csproj" />
<ProjectReference Include="..\Network\MUnique.OpenMU.Network.csproj" />
<ProjectReference Include="..\Network\Packets\MUnique.OpenMU.Network.Packets.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
// <copyright file="ClientPacketHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
using System.Net.Sockets;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using IConnectServer = MUnique.OpenMU.ConnectServer.IConnectServer;
/// <summary>
/// The handler of packets coming from the client.
/// </summary>
internal class ClientPacketHandler : IPacketHandler<Client>
{
private readonly ILogger<ClientPacketHandler> _logger;
private readonly IDictionary<byte, IPacketHandler<Client>> _packetHandlers = new Dictionary<byte, IPacketHandler<Client>>();
private readonly IConnectServerSettings _connectServerSettings;
/// <summary>
/// Initializes a new instance of the <see cref="ClientPacketHandler" /> class.
/// </summary>
/// <param name="connectServer">The connect server.</param>
/// <param name="loggerFactory">The logger factory.</param>
public ClientPacketHandler(IConnectServer connectServer, ILoggerFactory loggerFactory)
{
this._logger = loggerFactory.CreateLogger<ClientPacketHandler>();
this._connectServerSettings = connectServer.Settings;
// TODO: Is 0x05 correct? PatchCheckRequest has Code 0x02
this._packetHandlers.Add(0x05, new FtpRequestHandler(connectServer.Settings, loggerFactory.CreateLogger<FtpRequestHandler>()));
this._packetHandlers.Add(0xF4, new ServerListHandler(connectServer, loggerFactory));
}
/// <inheritdoc/>
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
{
try
{
if (packet.Length > this._connectServerSettings.MaximumReceiveSize || packet.Length < 4)
{
await this.DisconnectClientUnknownPacketAsync(client, packet).ConfigureAwait(false);
return;
}
var packetType = packet.Span[2];
if (this._packetHandlers.TryGetValue(packetType, out var packetHandler))
{
await packetHandler.HandlePacketAsync(client, packet).ConfigureAwait(false);
}
else if (this._connectServerSettings.DisconnectOnUnknownPacket)
{
await this.DisconnectClientUnknownPacketAsync(client, packet).ConfigureAwait(false);
}
else
{
// do nothing.
}
}
catch (SocketException ex)
{
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug("SocketException occured in Client.ReceivePacket, Client Address: {0}:{1}, Packet: [{2}], Exception: {3}", client.Address, client.Port, packet.ToArray().ToHexString(), ex);
}
}
catch (Exception ex)
{
this._logger.LogWarning("Exception occured in Client.ReceivePacket, Client Address: {0}:{1}, Packet: [{2}], Exception: {3}", client.Address, client.Port, packet.ToArray().ToHexString(), ex);
}
}
private async ValueTask DisconnectClientUnknownPacketAsync(Client client, Memory<byte> packet)
{
this._logger.LogInformation("Client {0}:{1} will be disconnected because it sent an unknown packet: {2}", client.Address, client.Port, packet.ToArray().ToHexString());
await client.Connection.DisconnectAsync().ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,154 @@
// <copyright file="FtpRequestHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ConnectServer;
/// <summary>
/// Handles the ftp related request. The client is sending its version, and the server answers
/// with the current version and the ftp address where the client can load a patch.
/// </summary>
internal class FtpRequestHandler : IPacketHandler<Client>
{
private static readonly byte[] PatchOk = { 0xC1, 4, 2, 0 };
private static readonly byte[] Xor3Keys = { 0xFC, 0xCF, 0xAB };
private readonly IConnectServerSettings _connectServerSettings;
private readonly ILogger<FtpRequestHandler> _logger;
private byte[]? _patchPacket;
/// <summary>
/// Initializes a new instance of the <see cref="FtpRequestHandler" /> class.
/// </summary>
/// <param name="connectServerSettings">The settings.</param>
/// <param name="logger">The logger.</param>
public FtpRequestHandler(IConnectServerSettings connectServerSettings, ILogger<FtpRequestHandler> logger)
{
this._connectServerSettings = connectServerSettings;
this._logger = logger;
}
/// <summary>
/// The version compare result.
/// </summary>
private enum VersionCompareResult
{
VersionTooLow = -1,
VersionMatch = 0,
VersionHigher = 1,
}
/// <inheritdoc/>
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
{
if (packet.Length < 6)
{
return;
}
void LogVersion(Span<byte> span)
{
this._logger.LogDebug($"Client {client.Address}:{client.Port} version: {span[3]}.{span[4]}.{span[5]}");
}
if (this._logger.IsEnabled(LogLevel.Debug))
{
LogVersion(packet.Span);
}
if (client.FtpRequestCount >= this._connectServerSettings.MaxFtpRequests)
{
if (this._logger.IsEnabled(LogLevel.Debug))
{
this._logger.LogDebug("Client {0}:{1} reached maxFtpRequests", client.Address, client.Port);
}
await client.Connection.DisconnectAsync().ConfigureAwait(false);
return;
}
int WritePatchPacket()
{
if (this._patchPacket is { } cachedPacket)
{
var span = client.Connection.Output.GetSpan(cachedPacket.Length);
cachedPacket.CopyTo(span);
}
else
{
var length = ClientNeedsPatchRef.Length;
var span = client.Connection.Output.GetSpan(length)[..length];
var packet = new ClientNeedsPatchRef(span);
packet.PatchAddress = this._connectServerSettings.PatchAddress;
var addressSize = Encoding.UTF8.GetByteCount(this._connectServerSettings.PatchAddress);
Xor3Bytes(span.Slice(6), addressSize);
packet.PatchVersion = this._connectServerSettings.CurrentPatchVersion[2];
this._patchPacket = span.ToArray();
}
return this._patchPacket.Length;
}
int WriteOkayPacket()
{
var span = client.Connection.Output.GetSpan(PatchOk.Length)[..PatchOk.Length];
PatchOk.CopyTo(span);
return PatchOk.Length;
}
if (VersionCompare(this._connectServerSettings.CurrentPatchVersion, 0, packet.Span, 3, this._connectServerSettings.CurrentPatchVersion.Length) == VersionCompareResult.VersionTooLow)
{
await client.Connection.SendAsync(WritePatchPacket).ConfigureAwait(false);
}
else
{
await client.Connection.SendAsync(WriteOkayPacket).ConfigureAwait(false);
}
client.FtpRequestCount++;
}
/// <summary>
/// Compares the actual version of the client with the expected version.
/// </summary>
/// <param name="expectedVersion">The expected version.</param>
/// <param name="expectedIndex">The expected index.</param>
/// <param name="actualVersion">The actual version.</param>
/// <param name="actualIndex">The actual index.</param>
/// <param name="count">The count.</param>
/// <returns>The compare result.</returns>
private static VersionCompareResult VersionCompare(byte[] expectedVersion, int expectedIndex, Span<byte> actualVersion, int actualIndex, int count)
{
for (int i = 0; i < count; ++i)
{
if (expectedVersion[i + expectedIndex] > actualVersion[i + actualIndex])
{
return VersionCompareResult.VersionTooLow;
}
if (expectedVersion[i + expectedIndex] < actualVersion[i + actualIndex])
{
return VersionCompareResult.VersionHigher;
}
}
return VersionCompareResult.VersionMatch;
}
private static void Xor3Bytes(Span<byte> data, int size)
{
for (int i = 0; i < size; i++)
{
data[i] ^= Xor3Keys[i % 3];
}
}
}

View File

@@ -0,0 +1,19 @@
// <copyright file="IPacketHandler{T}.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
/// <summary>
/// The interface for a packet handler with a type which is passed as context argument.
/// </summary>
/// <typeparam name="T">Type of the context argument.</typeparam>
internal interface IPacketHandler<in T>
{
/// <summary>
/// Handles the packet.
/// </summary>
/// <param name="obj">The object.</param>
/// <param name="packet">The packet.</param>
ValueTask HandlePacketAsync(T obj, Memory<byte> packet);
}

View File

@@ -0,0 +1,115 @@
// <copyright file="ServerInfoRequestHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
using System.Net;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Network;
using MUnique.OpenMU.Network.Packets.ConnectServer;
/// <summary>
/// Handles the server info request of a client, which means the client wants to know the connect data of the server it just clicked on.
/// </summary>
internal class ServerInfoRequestHandler : IPacketHandler<Client>
{
private readonly IConnectServer _connectServer;
private readonly ILogger<ServerInfoRequestHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ServerInfoRequestHandler" /> class.
/// </summary>
/// <param name="connectServer">The connect server.</param>
/// <param name="logger">The logger.</param>
public ServerInfoRequestHandler(IConnectServer connectServer, ILogger<ServerInfoRequestHandler> logger)
{
this._connectServer = connectServer;
this._logger = logger;
}
/// <inheritdoc/>
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
{
var serverId = GetServerId(packet.Span);
this._logger.LogDebug("Client {0}:{1} requested Connection Info of ServerId {2}", client.Address, client.Port, serverId);
if (client.ServerInfoRequestCount >= this._connectServer.Settings.MaxIpRequests)
{
this._logger.LogDebug($"Client {client.Address}:{client.Port} reached max ip requests.");
await client.Connection.DisconnectAsync().ConfigureAwait(false);
}
// First we look, if we can just use the IP address which the client connected to.
// If the game server is running on the same ip as the connect server, we can use that.
// This way, we can be sure, that the client can connect to it, too.
var localIpEndPoint = client.Connection.LocalEndPoint as IPEndPoint;
var serverItem = this._connectServer.ServerList.GetItem(serverId);
var isGameServerOnSameMachineAsConnectServer = (serverItem?.EndPoint.Address).IsOnSameHost();
var isClientConnectedOnNonRegisteredAddress = !object.Equals(serverItem?.EndPoint.Address, localIpEndPoint?.Address);
bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"), out var isRunningOnDocker);
// Only if we can't use the cached data.
if (isGameServerOnSameMachineAsConnectServer
&& !isRunningOnDocker
&& isClientConnectedOnNonRegisteredAddress)
{
int WritePacket()
{
var data = client.Connection.Output.GetSpan(ConnectionInfoRef.Length)[..ConnectionInfoRef.Length];
_ = new ConnectionInfoRef(data)
{
IpAddress = localIpEndPoint!.Address.ToString(),
Port = (ushort)serverItem!.EndPoint.Port,
};
return data.Length;
}
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
}
else if (this._connectServer.ConnectInfos.TryGetValue(serverId, out var connectInfo))
{
// more optimal way, because the serialized data was cached.
int WritePacket()
{
var span = client.Connection.Output.GetSpan(connectInfo.Length)[..connectInfo.Length];
connectInfo.CopyTo(span);
return span.Length;
}
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
}
else
{
this._logger.LogDebug($"Client {client.Address}:{client.Port}: Connection Info not found, sending Server List instead.");
int WritePacket()
{
var serverList = this._connectServer.ServerList.Serialize();
var span = client.Connection.Output.GetSpan(serverList.Length)[..serverList.Length];
serverList.CopyTo(span);
return span.Length;
}
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
}
await client.SendHelloAsync().ConfigureAwait(false);
client.ServerInfoRequestCount++;
}
private static ushort GetServerId(Span<byte> packet)
{
if (packet.Length == ConnectionInfoRequestRef.Length)
{
ConnectionInfoRequestRef data = packet;
return data.ServerId;
}
if (packet.Length == ConnectionInfoRequest075Ref.Length)
{
ConnectionInfoRequest075Ref data = packet;
return data.ServerId;
}
throw new ArgumentException($"Unknown packet length C1 {packet.Length} F4 03 ...");
}
}

View File

@@ -0,0 +1,54 @@
// <copyright file="ServerListHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Interfaces;
using IConnectServer = MUnique.OpenMU.ConnectServer.IConnectServer;
/// <summary>
/// Handles the requests of server data.
/// </summary>
internal class ServerListHandler : IPacketHandler<Client>
{
private readonly ILogger<ServerListHandler> _logger;
private readonly IConnectServerSettings _connectServerSettings;
private readonly IDictionary<byte, IPacketHandler<Client>> _packetHandlers = new Dictionary<byte, IPacketHandler<Client>>();
/// <summary>
/// Initializes a new instance of the <see cref="ServerListHandler" /> class.
/// </summary>
/// <param name="connectServer">The connect server.</param>
/// <param name="loggerFactory">The logger factory.</param>
public ServerListHandler(IConnectServer connectServer, ILoggerFactory loggerFactory)
{
this._logger = loggerFactory.CreateLogger<ServerListHandler>();
this._connectServerSettings = connectServer.Settings;
this._packetHandlers.Add(0x03, new ServerInfoRequestHandler(connectServer, loggerFactory.CreateLogger<ServerInfoRequestHandler>()));
this._packetHandlers.Add(0x06, new ServerListRequestHandler(connectServer, loggerFactory.CreateLogger<ServerListRequestHandler>()));
// old protocol:
this._packetHandlers.Add(0x02, new ServerListRequestHandler(connectServer, loggerFactory.CreateLogger<ServerListRequestHandler>()));
}
/// <inheritdoc/>
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
{
var packetSubType = packet.Span[3];
if (this._packetHandlers.TryGetValue(packetSubType, out var packetHandler))
{
await packetHandler.HandlePacketAsync(client, packet).ConfigureAwait(false);
}
else if (this._connectServerSettings.DisconnectOnUnknownPacket)
{
this._logger.LogInformation("Client {0}:{1} will be disconnected because it sent an unknown packet: {2}", client.Address, client.Port, packet.ToArray().ToHexString());
await client.Connection.DisconnectAsync().ConfigureAwait(false);
}
else
{
// do nothing
}
}
}

View File

@@ -0,0 +1,51 @@
// <copyright file="ServerListRequestHandler.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer.PacketHandler;
using Microsoft.Extensions.Logging;
using MUnique.OpenMU.Network;
/// <summary>
/// Handles the request of the server list.
/// </summary>
internal class ServerListRequestHandler : IPacketHandler<Client>
{
private readonly IConnectServer _connectServer;
private readonly ILogger<ServerListRequestHandler> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="ServerListRequestHandler" /> class.
/// </summary>
/// <param name="connectServer">The connect server.</param>
/// <param name="logger">The logger.</param>
public ServerListRequestHandler(IConnectServer connectServer, ILogger<ServerListRequestHandler> logger)
{
this._connectServer = connectServer;
this._logger = logger;
}
/// <inheritdoc/>
public async ValueTask HandlePacketAsync(Client client, Memory<byte> packet)
{
this._logger.LogDebug("Client {0}:{1} requested Server List", client.Address, client.Port);
if (client.ServerListRequestCount >= this._connectServer.Settings.MaxServerListRequests)
{
this._logger.LogDebug("Client {0}:{1} reached maxListRequests", client.Address, client.Port);
await client.Connection.DisconnectAsync().ConfigureAwait(false);
}
client.ServerListRequestCount++;
int WritePacket()
{
var serverList = this._connectServer.ServerList.Serialize();
var span = client.Connection.Output.GetSpan(serverList.Length)[..serverList.Length];
serverList.CopyTo(span);
return span.Length;
}
await client.Connection.SendAsync(WritePacket).ConfigureAwait(false);
}
}

View File

@@ -0,0 +1,10 @@
// <copyright file="AssemblyInfo.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
using System.Reflection;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MUnique.OpenMU.ConnectServer")]

View File

@@ -0,0 +1,214 @@
// <copyright file="ServerList.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Threading;
using MUnique.OpenMU.Network.Packets.ConnectServer;
using MUnique.OpenMU.Network.PlugIns;
/// <summary>
/// The server list.
/// </summary>
internal class ServerList
{
private readonly ReaderWriterLockSlim _lock = new();
private readonly ICollection<ServerListItem> _servers = new SortedSet<ServerListItem>(new ServerListItemComparer());
private readonly ClientVersion _clientVersion;
/// <summary>
/// Initializes a new instance of the <see cref="ServerList" /> class.
/// </summary>
/// <param name="clientVersion">The client version.</param>
public ServerList(ClientVersion clientVersion)
{
this._clientVersion = clientVersion;
}
/// <summary>
/// Gets the total connection count.
/// </summary>
public int TotalConnectionCount
{
get
{
this._lock.EnterReadLock();
try
{
return this._servers.Sum(s => s.CurrentConnections);
}
finally
{
this._lock.ExitReadLock();
}
}
}
/// <summary>
/// Gets the cache of the available servers.
/// </summary>
public byte[]? Cache { get; private set; }
/// <summary>
/// Gets the <see cref="IGameServerEntry"/>s of this list.
/// </summary>
public IEnumerable<IGameServerEntry> Items
{
get
{
this._lock.EnterReadLock();
try
{
return this._servers.ToList();
}
finally
{
this._lock.ExitReadLock();
}
}
}
/// <summary>
/// Adds the specified item to this instance.
/// </summary>
/// <param name="item">The item.</param>
public void Add(ServerListItem item)
{
this._lock.EnterWriteLock();
try
{
this._servers.Add(item);
this.InvalidateCache();
}
finally
{
this._lock.ExitWriteLock();
}
}
/// <summary>
/// Removes the specified item from this instance.
/// </summary>
/// <param name="item">The item.</param>
public void Remove(ServerListItem item)
{
this._lock.EnterWriteLock();
try
{
this._servers.Remove(item);
this.InvalidateCache();
}
finally
{
this._lock.ExitWriteLock();
}
}
/// <summary>
/// Gets the <see cref="ServerListItem"/> of the specified server id.
/// </summary>
/// <param name="gameServerId">The game server identifier.</param>
/// <returns>The found <see cref="ServerListItem"/>.</returns>
public ServerListItem? GetItem(ushort gameServerId)
{
this._lock.EnterReadLock();
try
{
return this._servers.FirstOrDefault(s => s.ServerId == gameServerId);
}
finally
{
this._lock.ExitReadLock();
}
}
/// <summary>
/// Serializes this instance to a server list packet, which can be sent to the client.
/// </summary>
/// <returns>The serialized server list.</returns>
public byte[] Serialize()
{
var result = this.Cache;
if (result != null)
{
return result;
}
this._lock.EnterReadLock();
try
{
result = this.Cache;
if (result != null)
{
return result;
}
byte[] packet;
if (this._clientVersion.Season == 0)
{
packet = new byte[ServerListResponseOld.GetRequiredSize(this._servers.Count)];
var response = new ServerListResponseOld(packet)
{
ServerCount = (byte)this._servers.Count,
};
var i = 0;
foreach (var server in this._servers)
{
var serverBlock = response[i];
serverBlock.ServerId = (byte)server.ServerId;
serverBlock.LoadPercentage = server.ServerLoadPercentage;
i++;
}
}
else
{
packet = new byte[ServerListResponse.GetRequiredSize(this._servers.Count)];
var response = new ServerListResponse(packet)
{
ServerCount = (ushort)this._servers.Count,
};
var i = 0;
foreach (var server in this._servers)
{
var serverBlock = response[i];
serverBlock.ServerId = server.ServerId;
serverBlock.LoadPercentage = server.ServerLoadPercentage;
i++;
}
}
this.Cache = packet;
return packet;
}
finally
{
this._lock.ExitReadLock();
}
}
/// <summary>
/// Invalidates the cache.
/// </summary>
private void InvalidateCache()
{
this.Cache = null;
}
/// <summary>
/// Comparer for <see cref="ServerListItem"/>s.
/// </summary>
private class ServerListItemComparer : IComparer<ServerListItem>
{
/// <summary>Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.</summary>
/// <returns>A signed integer that indicates the relative values of <paramref name="x" /> and <paramref name="y" />, as shown in the following table.Value Meaning Less than zero<paramref name="x" /> is less than <paramref name="y" />.Zero<paramref name="x" /> equals <paramref name="y" />.Greater than zero<paramref name="x" /> is greater than <paramref name="y" />.</returns>
/// <param name="x">The first object to compare.</param>
/// <param name="y">The second object to compare.</param>
public int Compare(ServerListItem? x, ServerListItem? y)
{
return x?.ServerId.CompareTo(y?.ServerId) ?? int.MinValue;
}
}
}

View File

@@ -0,0 +1,113 @@
// <copyright file="ServerListItem.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
using System.Net;
using MUnique.OpenMU.Network.Packets.ConnectServer;
/// <summary>
/// A list item of an available server.
/// </summary>
internal class ServerListItem : IGameServerEntry
{
private readonly ServerList _owner;
private byte _serverLoadPercentage;
private int _currentConnections;
/// <summary>
/// Initializes a new instance of the <see cref="ServerListItem"/> class.
/// </summary>
/// <param name="owner">The owner.</param>
public ServerListItem(ServerList owner)
{
this.LoadIndex = -1;
this._owner = owner;
this.ConnectInfo = new byte[] { 0xC1, 0x16, 0xF4, 0x03, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
}
/// <summary>
/// Gets or sets the index of the server in the load (usage rate) array.
/// </summary>
/// <value>
/// The index of the server in the load (usage rate) array.
/// </value>
public int LoadIndex { get; set; }
/// <summary>
/// Gets or sets the server identifier.
/// </summary>
/// <value>
/// The server identifier.
/// </value>
public ushort ServerId { get; set; }
/// <summary>
/// Gets the server load (usage rate).
/// </summary>
public byte ServerLoadPercentage
{
get => this._serverLoadPercentage;
private set
{
this._serverLoadPercentage = value;
var cache = this._owner.Cache;
if (cache != null && this.LoadIndex != -1)
{
cache[this.LoadIndex] = value;
}
}
}
/// <summary>
/// Gets the connect information.
/// </summary>
public byte[] ConnectInfo { get; }
/// <summary>
/// Gets or sets the ip end point.
/// </summary>
public IPEndPoint EndPoint
{
get
{
ConnectionInfoRef connectInfo = this.ConnectInfo.AsSpan();
return new IPEndPoint(IPAddress.Parse(connectInfo.IpAddress), connectInfo.Port);
}
set
{
this.ConnectInfo.AsSpan().Clear();
ConnectionInfo connectInfo = new ConnectionInfo(this.ConnectInfo);
connectInfo.IpAddress = value.Address.ToString();
connectInfo.Port = (ushort)value.Port;
}
}
/// <summary>
/// Gets or sets the maximum connection count.
/// </summary>
public int MaximumConnections { get; set; }
/// <summary>
/// Gets or sets the current connection count.
/// </summary>
public int CurrentConnections
{
get => this._currentConnections;
set
{
this._currentConnections = value;
this.ServerLoadPercentage = (byte)(this._currentConnections * 100f / this.MaximumConnections);
}
}
/// <inheritdoc/>
public override string ToString()
{
return $"ServerId={this.ServerId}, ServerLoadPercentage={this.ServerLoadPercentage}";
}
}

View File

@@ -0,0 +1,21 @@
// <copyright file="Utilities.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.ConnectServer;
/// <summary>
/// Some utility functions.
/// </summary>
internal static class Utilities
{
/// <summary>
/// Converts a byte array to a hexadecimal string, each byte separated by space.
/// </summary>
/// <param name="bytes">The byte array.</param>
/// <returns>The hexadecimal string.</returns>
public static string ToHexString(this byte[] bytes)
{
return BitConverter.ToString(bytes).Replace('-', ' ');
}
}