diff --git a/EtumrepMMO.Server.WinForms/Main.cs b/EtumrepMMO.Server.WinForms/Main.cs index daabb51..5c6089b 100644 --- a/EtumrepMMO.Server.WinForms/Main.cs +++ b/EtumrepMMO.Server.WinForms/Main.cs @@ -1,204 +1,203 @@ using Newtonsoft.Json; -namespace EtumrepMMO.Server.WinForms +namespace EtumrepMMO.Server.WinForms; + +public sealed partial class Main : Form { - public sealed partial class Main : Form + private readonly ServerConnection Connection; + private readonly string ConfigPath = GetConfigPath(); + private readonly object _logLock = new(); + private readonly object _queueLock = new(); + private readonly object _concurrentLock = new(); + + private static CancellationTokenSource Source { get; set; } = new(); + private ServerSettings Settings { get; set; } + private static bool WasStarted { get; set; } + + private const string _waiting = "Waiting for users..."; + private const string _noQueue = "No users in queue..."; + private const string _connectionsText = "Connections accepted: "; + private const string _authText = "Users authenticated: "; + private const string _etumrepText = "EtumrepMMOs run: "; + + public Main() { - private readonly ServerConnection Connection; - private readonly string ConfigPath = GetConfigPath(); - private readonly object _logLock = new(); - private readonly object _queueLock = new(); - private readonly object _concurrentLock = new(); - - private static CancellationTokenSource Source { get; set; } = new(); - private ServerSettings Settings { get; set; } - private static bool WasStarted { get; set; } - - private const string _waiting = "Waiting for users..."; - private const string _noQueue = "No users in queue..."; - private const string _connectionsText = "Connections accepted: "; - private const string _authText = "Users authenticated: "; - private const string _etumrepText = "EtumrepMMOs run: "; - - public Main() + InitializeComponent(); + if (File.Exists(ConfigPath)) { - InitializeComponent(); - if (File.Exists(ConfigPath)) - { - var text = File.ReadAllText(ConfigPath); - Settings = JsonConvert.DeserializeObject(text, GetSettings()) ?? new ServerSettings(); - UpdateLabels(Settings.ConnectionsAccepted, Settings.UsersAuthenticated, Settings.EtumrepsRun); - } - else Settings = new(); - - var status = new Progress(x => - { - UpdateStatusLamp(x); - }); + var text = File.ReadAllText(ConfigPath); + Settings = JsonConvert.DeserializeObject(text, GetSettings()) ?? new ServerSettings(); + UpdateLabels(Settings.ConnectionsAccepted, Settings.UsersAuthenticated, Settings.EtumrepsRun); + } + else Settings = new(); - var concurrent = new Progress<(string, bool)>(x => - { - UpdateCurrentlyProcessed(x.Item1, x.Item2); - }); + var status = new Progress(x => + { + UpdateStatusLamp(x); + }); - var labels = new Progress<(int, int, int)>(x => - { - UpdateLabels(x.Item1, x.Item2, x.Item3); - }); + var concurrent = new Progress<(string, bool)>(x => + { + UpdateCurrentlyProcessed(x.Item1, x.Item2); + }); - var queue = new Progress<(string, bool)>(x => - { - UpdateQueue(x.Item1, x.Item2); - }); + var labels = new Progress<(int, int, int)>(x => + { + UpdateLabels(x.Item1, x.Item2, x.Item3); + }); - UpdateCurrentlyProcessed(_waiting, false); - UpdateQueue(_noQueue, false); + var queue = new Progress<(string, bool)>(x => + { + UpdateQueue(x.Item1, x.Item2); + }); - RTB_Logs.MaxLength = 32_767; - Connection = new(Settings, status, concurrent, labels, queue); - Grid_Settings.SelectedObject = Settings; - LogUtil.Forwarders.Add(PostLog); - } + UpdateCurrentlyProcessed(_waiting, false); + UpdateQueue(_noQueue, false); - private void Main_FormClosing(object sender, FormClosingEventArgs e) - { - WindowState = FormWindowState.Minimized; - Stop(); - } + RTB_Logs.MaxLength = 32_767; + Connection = new(Settings, status, concurrent, labels, queue); + Grid_Settings.SelectedObject = Settings; + LogUtil.Forwarders.Add(PostLog); + } - private void Button_Start_Click(object sender, EventArgs e) - { - if (WasStarted) - return; + private void Main_FormClosing(object sender, FormClosingEventArgs e) + { + WindowState = FormWindowState.Minimized; + Stop(); + } - WasStarted = true; - Tab_Logs.Select(); - RunServer(); - } + private void Button_Start_Click(object sender, EventArgs e) + { + if (WasStarted) + return; - private void Button_Stop_Click(object sender, EventArgs e) - { - if (WasStarted) - Stop(); - } + WasStarted = true; + Tab_Logs.Select(); + RunServer(); + } - private void Stop() - { - SaveSettings(); - Source.Cancel(); + private void Button_Stop_Click(object sender, EventArgs e) + { + if (WasStarted) + Stop(); + } - async Task WaitUntilDone() - { - await Connection.Stop().ConfigureAwait(false); - Source = new(); - WasStarted = false; - LV_Concurrent.Items.Clear(); - LV_Concurrent.Items.Add("Waiting for users..."); - LV_QueueList.Items.Clear(); - } - Task.WhenAny(WaitUntilDone(), Task.Delay(1_000)).ConfigureAwait(true).GetAwaiter().GetResult(); - LogUtil.Log("Server has been shut down.", "[Stop Button Event]"); - } + private void Stop() + { + SaveSettings(); + Source.Cancel(); - private void PostLog(string message, string identity) + async Task WaitUntilDone() { - var line = $"[{DateTime.Now:HH:mm:ss}] - {identity}: {message}{Environment.NewLine}"; - if (InvokeRequired) - Invoke((MethodInvoker)(() => UpdateLog(line))); - else UpdateLog(line); + await Connection.Stop().ConfigureAwait(false); + Source = new(); + WasStarted = false; + LV_Concurrent.Items.Clear(); + LV_Concurrent.Items.Add("Waiting for users..."); + LV_QueueList.Items.Clear(); } + Task.WhenAny(WaitUntilDone(), Task.Delay(1_000)).ConfigureAwait(true).GetAwaiter().GetResult(); + LogUtil.Log("Server has been shut down.", "[Stop Button Event]"); + } - // Taken from kwsch's SysBot - // https://github.com/kwsch/SysBot.NET/commit/27455c4d88f1f9df7dc94dd0e76f3a9bb44b6242 - private void UpdateLog(string line) - { - lock (_logLock) - { - // ghetto truncate - var rtb = RTB_Logs; - var text = rtb.Text; - var max = rtb.MaxLength; - if (text.Length + line.Length + 2 >= max) - rtb.Text = text[(max / 4)..]; - - rtb.AppendText(line); - rtb.ScrollToCaret(); - } - } + private void PostLog(string message, string identity) + { + var line = $"[{DateTime.Now:HH:mm:ss}] - {identity}: {message}{Environment.NewLine}"; + if (InvokeRequired) + Invoke((MethodInvoker)(() => UpdateLog(line))); + else UpdateLog(line); + } - private void RunServer() + // Taken from kwsch's SysBot + // https://github.com/kwsch/SysBot.NET/commit/27455c4d88f1f9df7dc94dd0e76f3a9bb44b6242 + private void UpdateLog(string line) + { + lock (_logLock) { - var token = Source.Token; - _ = Task.Run(async () => await Connection.MainAsync(token), token); + // ghetto truncate + var rtb = RTB_Logs; + var text = rtb.Text; + var max = rtb.MaxLength; + if (text.Length + line.Length + 2 >= max) + rtb.Text = text[(max / 4)..]; + + rtb.AppendText(line); + rtb.ScrollToCaret(); } + } - private static string GetConfigPath() => "config.json"; + private void RunServer() + { + var token = Source.Token; + _ = Task.Run(async () => await Connection.MainAsync(token), token); + } - private static JsonSerializerSettings GetSettings() => new() - { - Formatting = Formatting.Indented, - DefaultValueHandling = DefaultValueHandling.Include, - NullValueHandling = NullValueHandling.Ignore, - }; + private static string GetConfigPath() => "config.json"; - private void SaveSettings() - { - var lines = JsonConvert.SerializeObject(Settings, GetSettings()); - File.WriteAllText(ConfigPath, lines); - } + private static JsonSerializerSettings GetSettings() => new() + { + Formatting = Formatting.Indented, + DefaultValueHandling = DefaultValueHandling.Include, + NullValueHandling = NullValueHandling.Ignore, + }; - private void UpdateStatusLamp(ConnectionStatus status) => PB_Ready.BackColor = status switch - { - ConnectionStatus.Connecting => Color.Wheat, - ConnectionStatus.Connected => Color.LawnGreen, - _ => Color.WhiteSmoke - }; + private void SaveSettings() + { + var lines = JsonConvert.SerializeObject(Settings, GetSettings()); + File.WriteAllText(ConfigPath, lines); + } - private void UpdateLabels(int connections, int authentications, int etumreps) - { - Label_Connections.Text = _connectionsText + connections; - Label_Authenticated.Text = _authText + authentications; - Label_Etumreps.Text = _etumrepText + etumreps; - } + private void UpdateStatusLamp(ConnectionStatus status) => PB_Ready.BackColor = status switch + { + ConnectionStatus.Connecting => Color.Wheat, + ConnectionStatus.Connected => Color.LawnGreen, + _ => Color.WhiteSmoke + }; + + private void UpdateLabels(int connections, int authentications, int etumreps) + { + Label_Connections.Text = _connectionsText + connections; + Label_Authenticated.Text = _authText + authentications; + Label_Etumreps.Text = _etumrepText + etumreps; + } - private void UpdateQueue(string text, bool insert) + private void UpdateQueue(string text, bool insert) + { + lock (_queueLock) { - lock (_queueLock) + var item = LV_QueueList.FindItemWithText(_noQueue); + LV_QueueList.Items.Remove(item); + + if (insert) + LV_QueueList.Items.Add(text); + else { - var item = LV_QueueList.FindItemWithText(_noQueue); + item = LV_QueueList.FindItemWithText(text); LV_QueueList.Items.Remove(item); - if (insert) - LV_QueueList.Items.Add(text); - else - { - item = LV_QueueList.FindItemWithText(text); - LV_QueueList.Items.Remove(item); - - if (LV_QueueList.Items.Count is 0) - LV_QueueList.Items.Add(_noQueue); - } + if (LV_QueueList.Items.Count is 0) + LV_QueueList.Items.Add(_noQueue); } } + } - private void UpdateCurrentlyProcessed(string text, bool insert) + private void UpdateCurrentlyProcessed(string text, bool insert) + { + lock (_concurrentLock) { - lock (_concurrentLock) + var item = LV_Concurrent.FindItemWithText(_waiting); + LV_Concurrent.Items.Remove(item); + + if (insert) + LV_Concurrent.Items.Add(text); + else { - var item = LV_Concurrent.FindItemWithText(_waiting); + item = LV_Concurrent.FindItemWithText(text); LV_Concurrent.Items.Remove(item); - if (insert) - LV_Concurrent.Items.Add(text); - else - { - item = LV_Concurrent.FindItemWithText(text); - LV_Concurrent.Items.Remove(item); - - if (LV_Concurrent.Items.Count is 0) - LV_Concurrent.Items.Add(_waiting); - } + if (LV_Concurrent.Items.Count is 0) + LV_Concurrent.Items.Add(_waiting); } } } -} +} \ No newline at end of file diff --git a/EtumrepMMO.Server.WinForms/Program.cs b/EtumrepMMO.Server.WinForms/Program.cs index 2ad5445..bdc5121 100644 --- a/EtumrepMMO.Server.WinForms/Program.cs +++ b/EtumrepMMO.Server.WinForms/Program.cs @@ -1,16 +1,15 @@ -namespace EtumrepMMO.Server.WinForms +namespace EtumrepMMO.Server.WinForms; + +internal static class Program { - internal static class Program + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() { - /// - /// The main entry point for the application. - /// - [STAThread] - static void Main() - { - Application.EnableVisualStyles(); - Application.SetCompatibleTextRenderingDefault(false); - Application.Run(new Main()); - } + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + Application.Run(new Main()); } } diff --git a/EtumrepMMO.Server/Enums.cs b/EtumrepMMO.Server/Enums.cs index 3a9b5b6..b2c628f 100644 --- a/EtumrepMMO.Server/Enums.cs +++ b/EtumrepMMO.Server/Enums.cs @@ -1,9 +1,8 @@ -namespace EtumrepMMO.Server +namespace EtumrepMMO.Server; + +public enum ConnectionStatus { - public enum ConnectionStatus - { - NotConnected, - Connecting, - Connected, - } + NotConnected, + Connecting, + Connected, } diff --git a/EtumrepMMO.Server/EtumrepUtil.cs b/EtumrepMMO.Server/EtumrepUtil.cs index cf1435d..51a77e4 100644 --- a/EtumrepMMO.Server/EtumrepUtil.cs +++ b/EtumrepMMO.Server/EtumrepUtil.cs @@ -1,27 +1,26 @@ using PKHeX.Core; using EtumrepMMO.Lib; -namespace EtumrepMMO.Server +namespace EtumrepMMO.Server; + +public class EtumrepUtil { - public class EtumrepUtil + public static ulong CalculateSeed(List pkms) { - public static ulong CalculateSeed(List pkms) - { - var seed = GroupSeedFinder.FindSeed(pkms).Seed; - return seed; - } + var seed = GroupSeedFinder.FindSeed(pkms).Seed; + return seed; + } - public static List GetPokeList(byte[] data, int count) + public static List GetPokeList(byte[] data, int count) + { + List pks = new(); + for (int i = 0; i < count; i++) { - List pks = new(); - for (int i = 0; i < count; i++) - { - int ofs = 376 * i; - byte[] buf = data.Slice(ofs, 376); - var pk = EntityFormat.GetFromBytes(buf)!; - pks.Add(pk); - } - return pks; + int ofs = 376 * i; + byte[] buf = data.Slice(ofs, 376); + var pk = EntityFormat.GetFromBytes(buf)!; + pks.Add(pk); } + return pks; } } diff --git a/EtumrepMMO.Server/HostSettings.cs b/EtumrepMMO.Server/HostSettings.cs index 5a24d27..40b4d86 100644 --- a/EtumrepMMO.Server/HostSettings.cs +++ b/EtumrepMMO.Server/HostSettings.cs @@ -1,74 +1,73 @@ using System.ComponentModel; -namespace EtumrepMMO.Server +namespace EtumrepMMO.Server; + +public class ServerSettings { - public class ServerSettings + public override string ToString() => "Server Settings"; + private const string Startup = nameof(Startup); + private const string User = nameof(User); + private const string Counts = nameof(Counts); + + [Category(Startup), Description("Port.")] + public int Port { get; set; } = 80; + + [Category(Startup), Description("Token for client authorization.")] + public string Token { get; set; } = string.Empty; + + [Category(Startup), Description("Maximum EtumrepMMO queue size. Server will deny connections until the queue is below this value.")] + public int MaxQueue { get; set; } = 10; + + [Category(Startup), Description("Maximum concurrent EtumrepMMO instances to run.")] + public int MaxConcurrent { get; set; } = 2; + + [Category(Startup), Description("Whitelisted clients (bot hosts).")] + public List HostWhitelist { get; set; } = new(); + + [Category(Startup), Description("Blacklisted users.")] + public List UserBlacklist { get; set; } = new(); + + [Category(User), Description("Discord user object.")] + public class DiscordUser + { + public override string ToString() => $"{Username}"; + + [Category(User), Description("Discord user's username.")] + public string Username { get; set; } = string.Empty; + + [Category(User), Description("Discord user's numerical ID.")] + public ulong ID { get; set; } + + [Category(User), Description("Discord user's password.")] + public string Password { get; set; } = string.Empty; + } + + private int _connectionsAccepted; + private int _usersAuthenticated; + private int _etumrepsRun; + + [Category(Counts), Description("Connections accepted.")] + public int ConnectionsAccepted { - public override string ToString() => "Server Settings"; - private const string Startup = nameof(Startup); - private const string User = nameof(User); - private const string Counts = nameof(Counts); - - [Category(Startup), Description("Port.")] - public int Port { get; set; } = 80; - - [Category(Startup), Description("Token for client authorization.")] - public string Token { get; set; } = string.Empty; - - [Category(Startup), Description("Maximum EtumrepMMO queue size. Server will deny connections until the queue is below this value.")] - public int MaxQueue { get; set; } = 10; - - [Category(Startup), Description("Maximum concurrent EtumrepMMO instances to run.")] - public int MaxConcurrent { get; set; } = 2; - - [Category(Startup), Description("Whitelisted clients (bot hosts).")] - public List HostWhitelist { get; set; } = new(); - - [Category(Startup), Description("Blacklisted users.")] - public List UserBlacklist { get; set; } = new(); - - [Category(User), Description("Discord user object.")] - public class DiscordUser - { - public override string ToString() => $"{Username}"; - - [Category(User), Description("Discord user's username.")] - public string Username { get; set; } = string.Empty; - - [Category(User), Description("Discord user's numerical ID.")] - public ulong ID { get; set; } - - [Category(User), Description("Discord user's password.")] - public string Password { get; set; } = string.Empty; - } - - private int _connectionsAccepted; - private int _usersAuthenticated; - private int _etumrepsRun; - - [Category(Counts), Description("Connections accepted.")] - public int ConnectionsAccepted - { - get => _connectionsAccepted; - set => _connectionsAccepted = value; - } - - [Category(Counts), Description("Users authenticated.")] - public int UsersAuthenticated - { - get => _usersAuthenticated; - set => _usersAuthenticated = value; - } - - [Category(Counts), Description("EtumrepMMOs successfully run.")] - public int EtumrepsRun - { - get => _etumrepsRun; - set => _etumrepsRun = value; - } - - public void AddConnectionsAccepted() => Interlocked.Increment(ref _connectionsAccepted); - public void AddUsersAuthenticated() => Interlocked.Increment(ref _usersAuthenticated); - public void AddEtumrepsRun() => Interlocked.Increment(ref _etumrepsRun); + get => _connectionsAccepted; + set => _connectionsAccepted = value; } + + [Category(Counts), Description("Users authenticated.")] + public int UsersAuthenticated + { + get => _usersAuthenticated; + set => _usersAuthenticated = value; + } + + [Category(Counts), Description("EtumrepMMOs successfully run.")] + public int EtumrepsRun + { + get => _etumrepsRun; + set => _etumrepsRun = value; + } + + public void AddConnectionsAccepted() => Interlocked.Increment(ref _connectionsAccepted); + public void AddUsersAuthenticated() => Interlocked.Increment(ref _usersAuthenticated); + public void AddEtumrepsRun() => Interlocked.Increment(ref _etumrepsRun); } diff --git a/EtumrepMMO.Server/LogUtil.cs b/EtumrepMMO.Server/LogUtil.cs index 94173d0..9a28603 100644 --- a/EtumrepMMO.Server/LogUtil.cs +++ b/EtumrepMMO.Server/LogUtil.cs @@ -3,47 +3,45 @@ using NLog.Targets; using System.Text; -namespace EtumrepMMO.Server -{ - // Mostly borrowed from kwsch's SysBot - // https://github.com/kwsch/SysBot.NET/blob/master/SysBot.Base/Util/LogUtil.cs +namespace EtumrepMMO.Server; +// Mostly borrowed from kwsch's SysBot +// https://github.com/kwsch/SysBot.NET/blob/master/SysBot.Base/Util/LogUtil.cs - public static class LogUtil - { - public static readonly List> Forwarders = new(); - private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); +public static class LogUtil +{ + public static readonly List> Forwarders = new(); + private static readonly ILogger Logger = LogManager.GetCurrentClassLogger(); - public static DateTime LastLogged { get; private set; } = DateTime.Now; + public static DateTime LastLogged { get; private set; } = DateTime.Now; - static LogUtil() + static LogUtil() + { + var config = new LoggingConfiguration(); + Directory.CreateDirectory("logs"); + var logfile = new FileTarget("logfile") { - var config = new LoggingConfiguration(); - Directory.CreateDirectory("logs"); - var logfile = new FileTarget("logfile") - { - FileName = Path.Combine("logs", "EtumrepMMO.Server.txt"), - ConcurrentWrites = true, - ArchiveEvery = FileArchivePeriod.Day, - ArchiveNumbering = ArchiveNumberingMode.Date, - ArchiveFileName = Path.Combine("logs", "EtumrepMMO.Server.{#}.txt"), - ArchiveDateFormat = "yyyy-MM-dd", - MaxArchiveFiles = 7, - Encoding = Encoding.Unicode, - WriteBom = true, - }; - config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile); - LogManager.Configuration = config; - } + FileName = Path.Combine("logs", "EtumrepMMO.Server.txt"), + ConcurrentWrites = true, + ArchiveEvery = FileArchivePeriod.Day, + ArchiveNumbering = ArchiveNumberingMode.Date, + ArchiveFileName = Path.Combine("logs", "EtumrepMMO.Server.{#}.txt"), + ArchiveDateFormat = "yyyy-MM-dd", + MaxArchiveFiles = 7, + Encoding = Encoding.Unicode, + WriteBom = true, + }; + config.AddRule(LogLevel.Debug, LogLevel.Fatal, logfile); + LogManager.Configuration = config; + } - public static void Log(string text, string identity) => LogInternal(LogLevel.Info, text, identity); + public static void Log(string text, string identity) => LogInternal(LogLevel.Info, text, identity); - private static void LogInternal(LogLevel level, string text, string identity) - { - foreach (var fw in Forwarders) - fw(text, identity); + private static void LogInternal(LogLevel level, string text, string identity) + { + foreach (var fw in Forwarders) + fw(text, identity); - Logger.Log(level, $"{identity} {text}"); - LastLogged = DateTime.Now; - } + Logger.Log(level, $"{identity} {text}"); + LastLogged = DateTime.Now; } } diff --git a/EtumrepMMO.Server/ServerConnection.cs b/EtumrepMMO.Server/ServerConnection.cs index 1025939..5f8ab94 100644 --- a/EtumrepMMO.Server/ServerConnection.cs +++ b/EtumrepMMO.Server/ServerConnection.cs @@ -6,353 +6,352 @@ using System.Collections.Concurrent; using Newtonsoft.Json; -namespace EtumrepMMO.Server +namespace EtumrepMMO.Server; + +public class ServerConnection { - public class ServerConnection + private ServerSettings Settings { get; } + private TcpListener Listener { get; set; } + private BlockingCollection UserQueue { get; } + private static IProgress Status { get; set; } = default!; + private static IProgress<(string, bool)> ConcurrentQueue { get; set; } = default!; + private static IProgress<(int, int, int)> Labels { get; set; } = default!; + private static IProgress<(string, bool)> Queue { get; set; } = default!; + private bool IsStopped { get; set; } + + private readonly SemaphoreSlim _semaphore; + private int _entryID; + + public ServerConnection(ServerSettings settings, IProgress status, IProgress<(string, bool)> concurrent, IProgress<(int, int, int)> labels, IProgress<(string, bool)> queue) { - private ServerSettings Settings { get; } - private TcpListener Listener { get; set; } - private BlockingCollection UserQueue { get; } - private static IProgress Status { get; set; } = default!; - private static IProgress<(string, bool)> ConcurrentQueue { get; set; } = default!; - private static IProgress<(int, int, int)> Labels { get; set; } = default!; - private static IProgress<(string, bool)> Queue { get; set; } = default!; - private bool IsStopped { get; set; } - - private readonly SemaphoreSlim _semaphore; - private int _entryID; - - public ServerConnection(ServerSettings settings, IProgress status, IProgress<(string, bool)> concurrent, IProgress<(int, int, int)> labels, IProgress<(string, bool)> queue) - { - Settings = settings; - Status = status; - ConcurrentQueue = concurrent; - Labels = labels; - Queue = queue; - UserQueue = new(settings.MaxQueue); - _semaphore = new(settings.MaxConcurrent, settings.MaxConcurrent); - - Listener = new(IPAddress.Any, settings.Port); - Listener.Server.ReceiveTimeout = 60_000; - Listener.Server.SendTimeout = 60_000; - Listener.Server.LingerState = new(true, 20); - } - - internal class RemoteUser - { - public override string ToString() => $"{EntryID}. {(UserAuth is not null ? UserAuth.ToString() : "Unknown user")}"; - - internal RemoteUser(TcpClient client, AuthenticatedStream stream) - { - Client = client; - Stream = stream; - } - - public TcpClient Client { get; } - public AuthenticatedStream Stream { get; } - public UserAuth? UserAuth { get; set; } = new(); - public bool IsAuthenticated { get; set; } - public byte[] Buffer { get; } = new byte[1504]; - public int EntryID { get; set; } - } + Settings = settings; + Status = status; + ConcurrentQueue = concurrent; + Labels = labels; + Queue = queue; + UserQueue = new(settings.MaxQueue); + _semaphore = new(settings.MaxConcurrent, settings.MaxConcurrent); + + Listener = new(IPAddress.Any, settings.Port); + Listener.Server.ReceiveTimeout = 60_000; + Listener.Server.SendTimeout = 60_000; + Listener.Server.LingerState = new(true, 20); + } - internal class UserAuth - { - public override string ToString() => $"Host: {HostName} ({HostID}) | User: {SeedCheckerName} ({SeedCheckerID})"; - public string HostName { get; set; } = string.Empty; - public ulong HostID { get; set; } - public string HostPassword { get; set; } = string.Empty; - public string Token { get; set; } = string.Empty; - public string SeedCheckerName { get; set; } = string.Empty; - public ulong SeedCheckerID { get; set; } - } + internal class RemoteUser + { + public override string ToString() => $"{EntryID}. {(UserAuth is not null ? UserAuth.ToString() : "Unknown user")}"; - public async Task Stop() + internal RemoteUser(TcpClient client, AuthenticatedStream stream) { - if (!IsStopped) - { - LogUtil.Log("Stopping the TCP Listener...", "[TCP Listener]"); - Listener.Stop(); - IsStopped = true; - Status.Report(ConnectionStatus.NotConnected); - await Task.Delay(0_050).ConfigureAwait(false); - } + Client = client; + Stream = stream; } - public async Task MainAsync(CancellationToken token) - { - Status.Report(ConnectionStatus.Connecting); - Listener.Start(100); - IsStopped = false; + public TcpClient Client { get; } + public AuthenticatedStream Stream { get; } + public UserAuth? UserAuth { get; set; } = new(); + public bool IsAuthenticated { get; set; } + public byte[] Buffer { get; } = new byte[1504]; + public int EntryID { get; set; } + } - _ = Task.Run(async () => await RemoteUserQueue(token).ConfigureAwait(false), token); - _ = Task.Run(async () => await UpdateLabels(token).ConfigureAwait(false), token); + internal class UserAuth + { + public override string ToString() => $"Host: {HostName} ({HostID}) | User: {SeedCheckerName} ({SeedCheckerID})"; + public string HostName { get; set; } = string.Empty; + public ulong HostID { get; set; } + public string HostPassword { get; set; } = string.Empty; + public string Token { get; set; } = string.Empty; + public string SeedCheckerName { get; set; } = string.Empty; + public ulong SeedCheckerID { get; set; } + } - Status.Report(ConnectionStatus.Connected); - LogUtil.Log("Server initialized, waiting for connections...", "[TCP Listener]"); + public async Task Stop() + { + if (!IsStopped) + { + LogUtil.Log("Stopping the TCP Listener...", "[TCP Listener]"); + Listener.Stop(); + IsStopped = true; + Status.Report(ConnectionStatus.NotConnected); + await Task.Delay(0_050).ConfigureAwait(false); + } + } - while (!token.IsCancellationRequested) - { - bool pending; - try - { - pending = Listener.Pending(); - } - catch (Exception ex) - { - LogUtil.Log($"TCP Listener has crashed, trying to restart the connection.\n{ex.Message}", "[TCP Listener]"); - Listener = new(IPAddress.Any, Settings.Port); - Listener.Server.LingerState = new(true, 20); + public async Task MainAsync(CancellationToken token) + { + Status.Report(ConnectionStatus.Connecting); + Listener.Start(100); + IsStopped = false; - pending = false; - LogUtil.Log("TCP Listener was restarted, waiting for connections...", "[TCP Listener]"); - } + _ = Task.Run(async () => await RemoteUserQueue(token).ConfigureAwait(false), token); + _ = Task.Run(async () => await UpdateLabels(token).ConfigureAwait(false), token); - if (pending) - _ = Task.Run(async () => await AcceptPendingConnection(token).ConfigureAwait(false), token); - await Task.Delay(0_200, token).ConfigureAwait(false); - } - } + Status.Report(ConnectionStatus.Connected); + LogUtil.Log("Server initialized, waiting for connections...", "[TCP Listener]"); - private async Task AcceptPendingConnection(CancellationToken token) + while (!token.IsCancellationRequested) { - LogUtil.Log("A user is attempting to connect...", "[TCP Listener]"); - var remoteClient = await Listener.AcceptTcpClientAsync(token).ConfigureAwait(false); - Settings.AddConnectionsAccepted(); - - LogUtil.Log("A user has connected, authenticating the connection...", "[TCP Listener]"); - RemoteUser? user = await AuthenticateConnection(remoteClient).ConfigureAwait(false); - if (user is null || !user.IsAuthenticated) - { - DisposeStream(user); - return; - } - - LogUtil.Log("Connection authenticated, attempting to authenticate the user...", "[TCP Listener]"); - user.UserAuth = await AuthenticateUser(user, token).ConfigureAwait(false); - if (user.UserAuth is null) + bool pending; + try { - await SendServerConfirmation(user, false, token).ConfigureAwait(false); - DisposeStream(user); - return; + pending = Listener.Pending(); } - Settings.AddUsersAuthenticated(); - - bool enqueue = UserQueue.Count < UserQueue.BoundedCapacity; - await SendServerConfirmation(user, enqueue, token).ConfigureAwait(false); - - if (enqueue) + catch (Exception ex) { - LogUtil.Log($"{user.UserAuth.HostName} ({user.UserAuth.HostID}) was successfully authenticated, enqueueing...", "[TCP Listener]"); + LogUtil.Log($"TCP Listener has crashed, trying to restart the connection.\n{ex.Message}", "[TCP Listener]"); + Listener = new(IPAddress.Any, Settings.Port); + Listener.Server.LingerState = new(true, 20); - // Increment queue entry ID. - user.EntryID = Interlocked.Increment(ref _entryID); - ReportUserQueue(user.ToString(), true); - UserQueue.Add(user, token); - return; + pending = false; + LogUtil.Log("TCP Listener was restarted, waiting for connections...", "[TCP Listener]"); } - LogUtil.Log($"{user.UserAuth.HostName} ({user.UserAuth.HostID}) was successfully authenticated but the queue is full, closing the connection...", "[TCP Listener]"); - DisposeStream(user); + if (pending) + _ = Task.Run(async () => await AcceptPendingConnection(token).ConfigureAwait(false), token); + await Task.Delay(0_200, token).ConfigureAwait(false); } + } + + private async Task AcceptPendingConnection(CancellationToken token) + { + LogUtil.Log("A user is attempting to connect...", "[TCP Listener]"); + var remoteClient = await Listener.AcceptTcpClientAsync(token).ConfigureAwait(false); + Settings.AddConnectionsAccepted(); - private async Task RemoteUserQueue(CancellationToken token) + LogUtil.Log("A user has connected, authenticating the connection...", "[TCP Listener]"); + RemoteUser? user = await AuthenticateConnection(remoteClient).ConfigureAwait(false); + if (user is null || !user.IsAuthenticated) { - while (!token.IsCancellationRequested) - { - try - { - await _semaphore.WaitAsync(token).ConfigureAwait(false); - var user = UserQueue.Take(token); - _ = Task.Run(async () => await RunEtumrepAsync(user, token).ConfigureAwait(false), token); - } - catch (Exception ex) - { - LogUtil.Log($"Error occurred when queuing a user:\n{ex.Message}", "[User Queue]"); - _semaphore.Release(); - } - } + DisposeStream(user); + return; } - private async Task RunEtumrepAsync(RemoteUser user, CancellationToken token) + LogUtil.Log("Connection authenticated, attempting to authenticate the user...", "[TCP Listener]"); + user.UserAuth = await AuthenticateUser(user, token).ConfigureAwait(false); + if (user.UserAuth is null) { - if (user.UserAuth is null) - { - LogUtil.Log("UserAuth is null: Something went very, very wrong.", "[User Queue]"); - _semaphore.Release(); - DisposeStream(user); - return; - } - - var checker = $"{user.EntryID}. {user.UserAuth.SeedCheckerName} ({user.UserAuth.SeedCheckerID})"; - ReportCurrentlyProcessed(checker, true); - LogUtil.Log($"{user.UserAuth.HostName}: Attempting to read PKM data from {user.UserAuth.SeedCheckerName}.", "[User Queue]"); - - async Task EtumrepFunc() - { - int read, count; - try - { - read = await user.Stream.ReadAsync(user.Buffer, token).ConfigureAwait(false); - count = read / 376; - - if (read is 0 || count is < 2 || count is > 4) - { - LogUtil.Log($"{user.UserAuth.HostName}: Received an incorrect amount of data from {user.UserAuth.SeedCheckerName}.", "[User Queue]"); - return; - } - } - catch (Exception ex) - { - LogUtil.Log($"{user.UserAuth.HostName}: Error occurred while reading data from {user.UserAuth.SeedCheckerName}.\n{ex.Message}", "[User Queue]"); - return; - } - LogUtil.Log($"{user.UserAuth.HostName}: Beginning seed calculation for {user.UserAuth.SeedCheckerName}...", "[User Queue]"); + await SendServerConfirmation(user, false, token).ConfigureAwait(false); + DisposeStream(user); + return; + } + Settings.AddUsersAuthenticated(); - var list = EtumrepUtil.GetPokeList(user.Buffer, count); - var sw = new Stopwatch(); + bool enqueue = UserQueue.Count < UserQueue.BoundedCapacity; + await SendServerConfirmation(user, enqueue, token).ConfigureAwait(false); - sw.Start(); - var seed = EtumrepUtil.CalculateSeed(list); - sw.Stop(); + if (enqueue) + { + LogUtil.Log($"{user.UserAuth.HostName} ({user.UserAuth.HostID}) was successfully authenticated, enqueueing...", "[TCP Listener]"); - Settings.AddEtumrepsRun(); - LogUtil.Log($"{user.UserAuth.HostName}: Seed ({seed}) calculation for {user.UserAuth.SeedCheckerName} complete ({sw.Elapsed}). Attempting to send the result...", "[User Queue]"); + // Increment queue entry ID. + user.EntryID = Interlocked.Increment(ref _entryID); + ReportUserQueue(user.ToString(), true); + UserQueue.Add(user, token); + return; + } - var bytes = BitConverter.GetBytes(seed); - try - { - ReportUserQueue(user.ToString(), false); - await user.Stream.WriteAsync(bytes, token).ConfigureAwait(false); - LogUtil.Log($"{user.UserAuth.HostName}: Results were sent, removing from queue.", "[User Queue]"); - } - catch (Exception ex) - { - LogUtil.Log($"{user.UserAuth.HostName}: Error occurred while sending results to {user.UserAuth.SeedCheckerName}.\n{ex.Message}", "[User Queue]"); - } - } + LogUtil.Log($"{user.UserAuth.HostName} ({user.UserAuth.HostID}) was successfully authenticated but the queue is full, closing the connection...", "[TCP Listener]"); + DisposeStream(user); + } + private async Task RemoteUserQueue(CancellationToken token) + { + while (!token.IsCancellationRequested) + { try { - await EtumrepFunc().ConfigureAwait(false); + await _semaphore.WaitAsync(token).ConfigureAwait(false); + var user = UserQueue.Take(token); + _ = Task.Run(async () => await RunEtumrepAsync(user, token).ConfigureAwait(false), token); } catch (Exception ex) { - LogUtil.Log($"{user.UserAuth.HostName}: Error occurred while processing {user.UserAuth.SeedCheckerName}.\n{ex.Message}", "[EtumrepFunc]"); + LogUtil.Log($"Error occurred when queuing a user:\n{ex.Message}", "[User Queue]"); + _semaphore.Release(); } + } + } - ReportCurrentlyProcessed(checker, false); + private async Task RunEtumrepAsync(RemoteUser user, CancellationToken token) + { + if (user.UserAuth is null) + { + LogUtil.Log("UserAuth is null: Something went very, very wrong.", "[User Queue]"); _semaphore.Release(); DisposeStream(user); + return; } - private static async Task AuthenticateConnection(TcpClient client) + var checker = $"{user.EntryID}. {user.UserAuth.SeedCheckerName} ({user.UserAuth.SeedCheckerID})"; + ReportCurrentlyProcessed(checker, true); + LogUtil.Log($"{user.UserAuth.HostName}: Attempting to read PKM data from {user.UserAuth.SeedCheckerName}.", "[User Queue]"); + + async Task EtumrepFunc() { + int read, count; try { - var stream = client.GetStream(); - stream.Socket.ReceiveTimeout = 60_000; - stream.Socket.SendTimeout = 60_000; - - var authStream = new NegotiateStream(stream, false); - var user = new RemoteUser(client, authStream); + read = await user.Stream.ReadAsync(user.Buffer, token).ConfigureAwait(false); + count = read / 376; - await authStream.AuthenticateAsServerAsync().ConfigureAwait(false); - user.IsAuthenticated = true; - LogUtil.Log("Initial authentication complete.", "[Connection Authentication]"); - return user; + if (read is 0 || count is < 2 || count is > 4) + { + LogUtil.Log($"{user.UserAuth.HostName}: Received an incorrect amount of data from {user.UserAuth.SeedCheckerName}.", "[User Queue]"); + return; + } } catch (Exception ex) { - LogUtil.Log($"Failed to authenticate user.\n{ex.Message}", "[Connection Authentication]"); - return null; + LogUtil.Log($"{user.UserAuth.HostName}: Error occurred while reading data from {user.UserAuth.SeedCheckerName}.\n{ex.Message}", "[User Queue]"); + return; } - } + LogUtil.Log($"{user.UserAuth.HostName}: Beginning seed calculation for {user.UserAuth.SeedCheckerName}...", "[User Queue]"); - private async Task AuthenticateUser(RemoteUser user, CancellationToken token) - { - UserAuth? authObj = null; + var list = EtumrepUtil.GetPokeList(user.Buffer, count); + var sw = new Stopwatch(); + + sw.Start(); + var seed = EtumrepUtil.CalculateSeed(list); + sw.Stop(); + + Settings.AddEtumrepsRun(); + LogUtil.Log($"{user.UserAuth.HostName}: Seed ({seed}) calculation for {user.UserAuth.SeedCheckerName} complete ({sw.Elapsed}). Attempting to send the result...", "[User Queue]"); + + var bytes = BitConverter.GetBytes(seed); try { - byte[] authBytes = new byte[688]; - await user.Stream.ReadAsync(authBytes, token).ConfigureAwait(false); - var text = Encoding.Unicode.GetString(authBytes); - authObj = JsonConvert.DeserializeObject(text); + ReportUserQueue(user.ToString(), false); + await user.Stream.WriteAsync(bytes, token).ConfigureAwait(false); + LogUtil.Log($"{user.UserAuth.HostName}: Results were sent, removing from queue.", "[User Queue]"); } catch (Exception ex) { - LogUtil.Log($"Failed to read user authentication data.\n{ex.Message}", "[User Authentication]"); - return null; + LogUtil.Log($"{user.UserAuth.HostName}: Error occurred while sending results to {user.UserAuth.SeedCheckerName}.\n{ex.Message}", "[User Queue]"); } + } - if (authObj is null) - { - LogUtil.Log("User did not send an authentication packet.", "[User Authentication]"); - return null; - } - else if (!Settings.HostWhitelist.Exists(x => x.ID == authObj.HostID && x.Password == authObj.HostPassword)) - { - LogUtil.Log($"{authObj.HostName} ({authObj.HostID}) is not a whitelisted bot host.", "[User Authentication]"); - return null; - } - else if (Settings.UserBlacklist.Exists(x => x.ID == authObj.SeedCheckerID)) - { - LogUtil.Log($"{authObj.SeedCheckerName} ({authObj.SeedCheckerID}) is a blacklisted user.", "[User Authentication]"); - return null; - } - else if (Settings.Token != authObj.Token) - { - LogUtil.Log($"The provided token ({authObj.Token}) does not match the token defined by us.", "[User Authentication]"); - return null; - } + try + { + await EtumrepFunc().ConfigureAwait(false); + } + catch (Exception ex) + { + LogUtil.Log($"{user.UserAuth.HostName}: Error occurred while processing {user.UserAuth.SeedCheckerName}.\n{ex.Message}", "[EtumrepFunc]"); + } + + ReportCurrentlyProcessed(checker, false); + _semaphore.Release(); + DisposeStream(user); + } + + private static async Task AuthenticateConnection(TcpClient client) + { + try + { + var stream = client.GetStream(); + stream.Socket.ReceiveTimeout = 60_000; + stream.Socket.SendTimeout = 60_000; + + var authStream = new NegotiateStream(stream, false); + var user = new RemoteUser(client, authStream); + + await authStream.AuthenticateAsServerAsync().ConfigureAwait(false); + user.IsAuthenticated = true; + LogUtil.Log("Initial authentication complete.", "[Connection Authentication]"); + return user; + } + catch (Exception ex) + { + LogUtil.Log($"Failed to authenticate user.\n{ex.Message}", "[Connection Authentication]"); + return null; + } + } - await SendServerConfirmation(user, true, token).ConfigureAwait(false); - return authObj; + private async Task AuthenticateUser(RemoteUser user, CancellationToken token) + { + UserAuth? authObj = null; + try + { + byte[] authBytes = new byte[688]; + await user.Stream.ReadAsync(authBytes, token).ConfigureAwait(false); + var text = Encoding.Unicode.GetString(authBytes); + authObj = JsonConvert.DeserializeObject(text); + } + catch (Exception ex) + { + LogUtil.Log($"Failed to read user authentication data.\n{ex.Message}", "[User Authentication]"); + return null; } - private static async Task SendServerConfirmation(RemoteUser user, bool confirmed, CancellationToken token) + if (authObj is null) { - try - { - var bytes = BitConverter.GetBytes(confirmed); - await user.Stream.WriteAsync(bytes.AsMemory(0, 1), token).ConfigureAwait(false); - } - catch (Exception ex) - { - LogUtil.Log($"Failed to send response to user.\n{ex.Message}", "[SendServerConfirmation]"); - } + LogUtil.Log("User did not send an authentication packet.", "[User Authentication]"); + return null; + } + else if (!Settings.HostWhitelist.Exists(x => x.ID == authObj.HostID && x.Password == authObj.HostPassword)) + { + LogUtil.Log($"{authObj.HostName} ({authObj.HostID}) is not a whitelisted bot host.", "[User Authentication]"); + return null; + } + else if (Settings.UserBlacklist.Exists(x => x.ID == authObj.SeedCheckerID)) + { + LogUtil.Log($"{authObj.SeedCheckerName} ({authObj.SeedCheckerID}) is a blacklisted user.", "[User Authentication]"); + return null; + } + else if (Settings.Token != authObj.Token) + { + LogUtil.Log($"The provided token ({authObj.Token}) does not match the token defined by us.", "[User Authentication]"); + return null; } - private async Task UpdateLabels(CancellationToken token) + await SendServerConfirmation(user, true, token).ConfigureAwait(false); + return authObj; + } + + private static async Task SendServerConfirmation(RemoteUser user, bool confirmed, CancellationToken token) + { + try { - while (!token.IsCancellationRequested) - { - await Task.Delay(0_100, token).ConfigureAwait(false); - Labels.Report((Settings.ConnectionsAccepted, Settings.UsersAuthenticated, Settings.EtumrepsRun)); - } + var bytes = BitConverter.GetBytes(confirmed); + await user.Stream.WriteAsync(bytes.AsMemory(0, 1), token).ConfigureAwait(false); + } + catch (Exception ex) + { + LogUtil.Log($"Failed to send response to user.\n{ex.Message}", "[SendServerConfirmation]"); + } + } + + private async Task UpdateLabels(CancellationToken token) + { + while (!token.IsCancellationRequested) + { + await Task.Delay(0_100, token).ConfigureAwait(false); + Labels.Report((Settings.ConnectionsAccepted, Settings.UsersAuthenticated, Settings.EtumrepsRun)); } + } - private static void ReportUserQueue(string name, bool insert) => Queue.Report((name, insert)); - private static void ReportCurrentlyProcessed(string name, bool insert) => ConcurrentQueue.Report((name, insert)); + private static void ReportUserQueue(string name, bool insert) => Queue.Report((name, insert)); + private static void ReportCurrentlyProcessed(string name, bool insert) => ConcurrentQueue.Report((name, insert)); - private static void DisposeStream(RemoteUser? user) + private static void DisposeStream(RemoteUser? user) + { + if (user is not null) { - if (user is not null) + try { - try - { - user.Client.Close(); - user.Stream.Dispose(); - } - catch (Exception ex) - { - string msg = string.Empty; - if (user.UserAuth is not null) - msg = $"{user.UserAuth.HostName}: "; + user.Client.Close(); + user.Stream.Dispose(); + } + catch (Exception ex) + { + string msg = string.Empty; + if (user.UserAuth is not null) + msg = $"{user.UserAuth.HostName}: "; - msg += $"Error occurred while disposing the connection stream.\n{ex.Message}"; - LogUtil.Log(msg, "[DisposeStream]"); - } + msg += $"Error occurred while disposing the connection stream.\n{ex.Message}"; + LogUtil.Log(msg, "[DisposeStream]"); } } }