From 1177465843f572dcf5239adf7e9af4546fe2f757 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 3 Aug 2025 12:14:07 +0100 Subject: [PATCH 1/6] First pass at moving network infrastructure out of game code --- Server/SMain.cs | 64 ++-- ServerLibrary/DBModels/AccountInfo.cs | 1 + .../Commands/Command/Admin/ClearIPBlocks.cs | 5 +- .../Commands/Command/Admin/GiveGameGold.cs | 4 +- .../Commands/Command/Admin/TakeCastle.cs | 28 +- .../Commands/ErrorHandlingCommandHandler.cs | 1 + ServerLibrary/Envir/SEnvir.cs | 219 ++------------ .../SConnection.cs | 38 +-- .../Infrastructure.Network/TcpServer.cs | 285 ++++++++++++++++++ ServerLibrary/Models/ConquestWar.cs | 10 +- ServerLibrary/Models/Map.cs | 7 +- ServerLibrary/Models/Monsters/CastleFlag.cs | 23 +- ServerLibrary/Models/Monsters/CastleLord.cs | 5 +- .../Models/Monsters/JinamStoneGate.cs | 7 +- .../Models/Monsters/NetherworldGate.cs | 8 +- ServerLibrary/Models/PlayerObject.cs | 69 +---- 16 files changed, 387 insertions(+), 387 deletions(-) rename ServerLibrary/{Envir => Infrastructure.Network}/SConnection.cs (97%) create mode 100644 ServerLibrary/Infrastructure.Network/TcpServer.cs diff --git a/Server/SMain.cs b/Server/SMain.cs index 54946bcc..6f32b1b2 100644 --- a/Server/SMain.cs +++ b/Server/SMain.cs @@ -8,6 +8,7 @@ using PluginCore; using Server.DBModels; using Server.Envir; +using Server.Infrastructure.Network; //TODO: this should really be exposed here... maybe create a ServerMetric class to share between the two. using Server.Views; using System; using System.Collections.Generic; @@ -142,6 +143,7 @@ protected override void OnClosing(CancelEventArgs e) if (SEnvir.EnvirThread == null) return; + SEnvir.Started = false; while (SEnvir.EnvirThread != null) Thread.Sleep(1); @@ -187,7 +189,7 @@ private void UpdateInterface() StartServerButton.Enabled = SEnvir.EnvirThread == null; StopServerButton.Enabled = SEnvir.Started; - ConnectionLabel.Caption = string.Format(@"Connections: {0:#,##0}", SEnvir.Connections.Count); + ConnectionLabel.Caption = string.Format(@"Connections: {0:#,##0}", TcpServer.Connections.Count); ObjectLabel.Caption = string.Format(@"Objects: {0} of {1:#,##0}", SEnvir.ActiveObjects.Count, SEnvir.Objects.Count); ProcessLabel.Caption = string.Format(@"Process Count: {0:#,##0}", SEnvir.ProcessObjectCount); LoopLabel.Caption = string.Format(@"Loop Count: {0:#,##0}", SEnvir.LoopCount); @@ -200,42 +202,42 @@ private void UpdateInterface() const decimal MB = KB * 1024; const decimal GB = MB * 1024; - if (SEnvir.TotalBytesReceived > GB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}GB", SEnvir.TotalBytesReceived / GB); - else if (SEnvir.TotalBytesReceived > MB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}MB", SEnvir.TotalBytesReceived / MB); - else if (SEnvir.TotalBytesReceived > KB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}KB", SEnvir.TotalBytesReceived / KB); + if (TcpServer.TotalBytesReceived > GB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}GB", TcpServer.TotalBytesReceived / GB); + else if (TcpServer.TotalBytesReceived > MB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}MB", TcpServer.TotalBytesReceived / MB); + else if (TcpServer.TotalBytesReceived > KB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}KB", TcpServer.TotalBytesReceived / KB); else - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}B", SEnvir.TotalBytesReceived); - - if (SEnvir.TotalBytesSent > GB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}GB", SEnvir.TotalBytesSent / GB); - else if (SEnvir.TotalBytesSent > MB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}MB", SEnvir.TotalBytesSent / MB); - else if (SEnvir.TotalBytesSent > KB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}KB", SEnvir.TotalBytesSent / KB); + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}B", TcpServer.TotalBytesReceived); + + if (TcpServer.TotalBytesSent > GB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}GB", TcpServer.TotalBytesSent / GB); + else if (TcpServer.TotalBytesSent > MB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}MB", TcpServer.TotalBytesSent / MB); + else if (TcpServer.TotalBytesSent > KB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}KB", TcpServer.TotalBytesSent / KB); else - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}B", SEnvir.TotalBytesSent); + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}B", TcpServer.TotalBytesSent); - if (SEnvir.DownloadSpeed > GB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}GBps", SEnvir.DownloadSpeed / GB); - else if (SEnvir.DownloadSpeed > MB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}MBps", SEnvir.DownloadSpeed / MB); - else if (SEnvir.DownloadSpeed > KB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}KBps", SEnvir.DownloadSpeed / KB); + if (TcpServer.DownloadSpeed > GB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}GBps", TcpServer.DownloadSpeed / GB); + else if (TcpServer.DownloadSpeed > MB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}MBps", TcpServer.DownloadSpeed / MB); + else if (TcpServer.DownloadSpeed > KB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}KBps", TcpServer.DownloadSpeed / KB); else - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}Bps", SEnvir.DownloadSpeed); - - if (SEnvir.UploadSpeed > GB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}GBps", SEnvir.UploadSpeed / GB); - else if (SEnvir.UploadSpeed > MB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}MBps", SEnvir.UploadSpeed / MB); - else if (SEnvir.UploadSpeed > KB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}KBps", SEnvir.UploadSpeed / KB); + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}Bps", TcpServer.DownloadSpeed); + + if (TcpServer.UploadSpeed > GB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}GBps", TcpServer.UploadSpeed / GB); + else if (TcpServer.UploadSpeed > MB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}MBps", TcpServer.UploadSpeed / MB); + else if (TcpServer.UploadSpeed > KB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}KBps", TcpServer.UploadSpeed / KB); else - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}Bps", SEnvir.UploadSpeed); + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}Bps", TcpServer.UploadSpeed); } private void StartServerButton_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) diff --git a/ServerLibrary/DBModels/AccountInfo.cs b/ServerLibrary/DBModels/AccountInfo.cs index 0164c24d..c5d6f6f6 100644 --- a/ServerLibrary/DBModels/AccountInfo.cs +++ b/ServerLibrary/DBModels/AccountInfo.cs @@ -2,6 +2,7 @@ using Library.SystemModels; using MirDB; using Server.Envir; +using Server.Infrastructure.Network; using System; using System.Collections.Generic; using System.Linq; diff --git a/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs b/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs index db6b4209..9fab7db1 100644 --- a/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs +++ b/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs @@ -1,4 +1,5 @@ -using Server.Models; +using Server.Infrastructure.Network; +using Server.Models; namespace Server.Envir.Commands.Command.Admin { @@ -8,7 +9,7 @@ class ClearIPBlocks : AbstractCommand public override void Action(PlayerObject player) { - SEnvir.IPBlocks.Clear(); + TcpServer.IPBlocks.Clear(); } } } diff --git a/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs b/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs index bd1ba5de..124f2257 100644 --- a/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs +++ b/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs @@ -3,9 +3,11 @@ using Server.Envir.Commands.Command; using Server.Envir.Commands.Command.Admin; using Server.Envir.Commands.Exceptions; +using Server.Infrastructure.Network; using Server.Models; -namespace Server.Envir.Commands.Admin { +namespace Server.Envir.Commands.Admin +{ class GiveGameGold : AbstractParameterizedCommand { public override string VALUE => "GIVEGAMEGOLD"; public override int PARAMS_LENGTH => 3; diff --git a/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs b/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs index 6c036027..8739ec4a 100644 --- a/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs +++ b/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs @@ -2,6 +2,7 @@ using Library.SystemModels; using Server.DBModels; using Server.Envir.Commands.Exceptions; +using Server.Infrastructure.Network; using Server.Models; using System; using System.Linq; @@ -30,19 +31,7 @@ public override void Action(PlayerObject player, string[] vals) throw new UserCommandException(string.Format("No guild currently owns {0} castle.", castle.Name)); ownerGuild.Castle = null; - foreach (SConnection con in SEnvir.Connections) - { - switch (con.Stage) - { - case GameStage.Game: - case GameStage.Observer: - con.ReceiveChat(string.Format(con.Language.ConquestLost, ownerGuild.GuildName, castle.Name), MessageType.System); - break; - default: - continue; - } - } - + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestLost, ownerGuild.GuildName, castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = castle.Index, Owner = string.Empty }); foreach (PlayerObject user in SEnvir.Players) @@ -53,18 +42,7 @@ public override void Action(PlayerObject player, string[] vals) else { player.Character.Account.GuildMember.Guild.Castle = castle; - foreach (SConnection con in SEnvir.Connections) - { - switch (con.Stage) - { - case GameStage.Game: - case GameStage.Observer: - con.ReceiveChat(string.Format(con.Language.ConquestCapture, player.Character.Account.GuildMember.Guild.GuildName, castle.Name), MessageType.System); - break; - default: - continue; - } - } + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, player.Character.Account.GuildMember.Guild.GuildName, castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = castle.Index, Owner = player.Character.Account.GuildMember.Guild.GuildName }); foreach (PlayerObject user in SEnvir.Players) diff --git a/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs b/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs index c8740d7c..eb92b6ae 100644 --- a/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs +++ b/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs @@ -1,6 +1,7 @@ using Library; using Server.Envir.Commands.Exceptions; using Server.Envir.Commands.Handler; +using Server.Infrastructure.Network; using Server.Models; using System; using System.Collections.Generic; diff --git a/ServerLibrary/Envir/SEnvir.cs b/ServerLibrary/Envir/SEnvir.cs index fd6a5a39..1e4ae435 100644 --- a/ServerLibrary/Envir/SEnvir.cs +++ b/ServerLibrary/Envir/SEnvir.cs @@ -7,6 +7,7 @@ using Server.Envir.Commands.Handler; using Server.Envir.Events; using Server.Envir.Events.Triggers; +using Server.Infrastructure.Network; using Server.Models; using System; using System.Collections.Concurrent; @@ -72,153 +73,30 @@ public static void LogChat(string log) #region Network - public static Dictionary IPBlocks = new Dictionary(); - public static Dictionary IPCount = new Dictionary(); - public static List Connections = new List(); - public static ConcurrentQueue NewConnections; - private static TcpListener _listener, _userCountListener; - - private static void StartNetwork(bool log = true) - { - try - { - NewConnections = new ConcurrentQueue(); - - _listener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.Port); - _listener.Start(); - _listener.BeginAcceptTcpClient(Connection, null); - - _userCountListener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.UserCountPort); - _userCountListener.Start(); - _userCountListener.BeginAcceptTcpClient(CountConnection, null); - - NetworkStarted = true; - if (log) Log($"Network Started. Listen: {Config.IPAddress}:{Config.Port}"); - } - catch (Exception ex) - { - Started = false; - Log(ex.ToString()); - } - } - private static void StopNetwork(bool log = true) - { - TcpListener expiredListener = _listener; - TcpListener expiredUserListener = _userCountListener; - - _listener = null; - _userCountListener = null; - - Started = false; - - expiredListener?.Stop(); - expiredUserListener?.Stop(); - - NewConnections = null; - - try - { - Packet p = new G.Disconnect { Reason = DisconnectReason.ServerClosing }; - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(p); - - Thread.Sleep(2000); - } - catch (Exception ex) - { - Log(ex.ToString()); - } - - if (log) Log("Network Stopped."); - } - - private static void Connection(IAsyncResult result) - { - try - { - if (_listener == null || !_listener.Server.IsBound) return; - - TcpClient client = _listener.EndAcceptTcpClient(result); - - string ipAddress = client.Client.RemoteEndPoint.ToString().Split(':')[0]; - - if (!IPBlocks.TryGetValue(ipAddress, out DateTime banDate) || banDate < Now) - { - SConnection Connection = new SConnection(client); - - if (Connection.Connected) - NewConnections?.Enqueue(Connection); - } - } - catch (SocketException) - { - - } - catch (Exception ex) - { - Log(ex.ToString()); - } - finally - { - while (NewConnections?.Count >= 15) - Thread.Sleep(1); - - if (_listener != null && _listener.Server.IsBound) - _listener.BeginAcceptTcpClient(Connection, null); - } - } - - private static void CountConnection(IAsyncResult result) - { - try - { - if (_userCountListener == null || !_userCountListener.Server.IsBound) return; - - TcpClient client = _userCountListener.EndAcceptTcpClient(result); - - byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", Connections.Count)); + #endregion - client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, CountConnectionEnd, client); - } - catch { } - finally + //TODO: make this public readonly - too much leakage, expose a SEnvir.Stop() method where we are tryingto do SEnvir.Started = false; + public static bool Started + { + get => TcpServer.NetworkStarted; + set { - if (_userCountListener != null && _userCountListener.Server.IsBound) - _userCountListener.BeginAcceptTcpClient(CountConnection, null); - } + if (TcpServer.NetworkStarted == value) return; + if (value) TcpServer.StartNetwork(); + else TcpServer.StopNetwork(); + } } - private static void CountConnectionEnd(IAsyncResult result) - { - try - { - TcpClient client = result.AsyncState as TcpClient; - - if (client == null) return; - - client.Client.EndSend(result); - - client.Client.Dispose(); - } - catch { } - } - - #endregion - - public static bool Started { get; set; } - public static bool NetworkStarted { get; set; } public static bool Saving { get; private set; } + + //TODO: why are we leaking thread implementation here - why not just reuse !SEnvir.Started? logic here? public static Thread EnvirThread { get; private set; } public static DateTime Now, StartTime, LastWarTime; public static int ProcessObjectCount, LoopCount; - public static long DBytesSent, DBytesReceived; - public static long TotalBytesSent, TotalBytesReceived; - public static long DownloadSpeed, UploadSpeed; - public static ICommandHandler CommandHandler = new ErrorHandlingCommandHandler( new PlayerCommandHandler(), new AdminCommandHandler() @@ -1031,15 +909,11 @@ public static void EnvirLoop() DateTime DBTime = Now + Config.DBSaveDelay; StartEnvir(); - StartNetwork(); - + TcpServer.StartNetwork(); WebServer.StartWebServer(); - Started = NetworkStarted; - int count = 0, loopCount = 0; DateTime nextCount = Now.AddSeconds(1), UserCountTime = Now.AddMinutes(5), EventTimerTime = Now.AddMinutes(1), saveTime; - long previousTotalSent = 0, previousTotalReceived = 0; int lastindex = 0; long conDelay = 0; Thread logThread = new Thread(WriteLogsLoop) { IsBackground = true }; @@ -1056,31 +930,7 @@ public static void EnvirLoop() try { - SConnection connection; - while (!NewConnections.IsEmpty) - { - if (!NewConnections.TryDequeue(out connection)) break; - - IPCount.TryGetValue(connection.IPAddress, out var ipCount); - - IPCount[connection.IPAddress] = ipCount + 1; - - Connections.Add(connection); - } - - long bytesSent = 0; - long bytesReceived = 0; - - for (int i = Connections.Count - 1; i >= 0; i--) - { - if (i >= Connections.Count) break; - - connection = Connections[i]; - - connection.Process(); - bytesSent += connection.TotalBytesSent; - bytesReceived += connection.TotalBytesReceived; - } + TcpServer.Process(); long delay = (Time.Now - Now).Ticks / TimeSpan.TicksPerMillisecond; if (delay > conDelay) @@ -1089,9 +939,6 @@ public static void EnvirLoop() for (int i = Players.Count - 1; i >= 0; i--) Players[i].StartProcess(); - TotalBytesSent = DBytesSent + bytesSent; - TotalBytesReceived = DBytesReceived + bytesReceived; - if (ServerBuffChanged) { for (int i = Players.Count - 1; i >= 0; i--) @@ -1152,31 +999,10 @@ public static void EnvirLoop() loopCount = 0; conDelay = 0; - DownloadSpeed = TotalBytesReceived - previousTotalReceived; - UploadSpeed = TotalBytesSent - previousTotalSent; - - previousTotalReceived = TotalBytesReceived; - previousTotalSent = TotalBytesSent; - if (Now >= UserCountTime) { UserCountTime = Now.AddMinutes(5); - - foreach (SConnection conn in Connections) - { - conn.ReceiveChat(string.Format(conn.Language.OnlineCount, Players.Count, Connections.Count(x => x.Stage == GameStage.Observer)), MessageType.Hint); - - switch (conn.Stage) - { - case GameStage.Game: - if (conn.Player.Character.Observable) - conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observers.Count), MessageType.Hint); - break; - case GameStage.Observer: - conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observed.Observers.Count), MessageType.Hint); - break; - } - } + TcpServer.BroadcastOnlineMessage(); } if (Now >= EventTimerTime) @@ -1276,9 +1102,7 @@ public static void EnvirLoop() Log(ex.StackTrace); File.AppendAllText(@".\Errors.txt", ex.StackTrace + Environment.NewLine); - Packet p = new G.Disconnect { Reason = DisconnectReason.Crashed }; - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(p); + TcpServer.Broadcast(new G.Disconnect { Reason = DisconnectReason.Crashed }); Thread.Sleep(3000); break; @@ -1286,7 +1110,7 @@ public static void EnvirLoop() } WebServer.StopWebServer(); - StopNetwork(); + TcpServer.StopNetwork(); while (Saving) Thread.Sleep(1); if (Session != null) @@ -2965,12 +2789,7 @@ public static void NewAccount(C.NewAccount p, SConnection con) if (nowcount > 2 || todaycount > 5) { - IPBlocks[con.IPAddress] = Now.AddDays(7); - - for (int i = Connections.Count - 1; i >= 0; i--) - if (Connections[i].IPAddress == con.IPAddress) - Connections[i].Disconnecting = true; - + TcpServer.IpBan(con.IPAddress, TimeSpan.FromDays(7)); Log($"{con.IPAddress} Disconnected and banned for trying too many accounts"); return; } diff --git a/ServerLibrary/Envir/SConnection.cs b/ServerLibrary/Infrastructure.Network/SConnection.cs similarity index 97% rename from ServerLibrary/Envir/SConnection.cs rename to ServerLibrary/Infrastructure.Network/SConnection.cs index 0fbb9c08..a811544d 100644 --- a/ServerLibrary/Envir/SConnection.cs +++ b/ServerLibrary/Infrastructure.Network/SConnection.cs @@ -2,6 +2,7 @@ using Library.Network; using Library.SystemModels; using Server.DBModels; +using Server.Envir; using Server.Envir.Translations; using Server.Models; using System; @@ -13,7 +14,7 @@ using G = Library.Network.GeneralPackets; using S = Library.Network.ServerPackets; -namespace Server.Envir +namespace Server.Infrastructure.Network { public sealed class SConnection : BaseConnection { @@ -37,6 +38,8 @@ public sealed class SConnection : BaseConnection public List MPSearchResults = new List(); public HashSet VisibleResults = new HashSet(); + //TODO: language should be owned by client, just send a numeric value and let the client translate it. + // new S.SystemMessage { id = 1 }; public StringMessages Language; public SConnection(TcpClient client) : base(client) @@ -65,18 +68,9 @@ public SConnection(TcpClient client) : base(client) public override void Disconnect() { if (!Connected) return; - base.Disconnect(); - CleanUp(); - - if (!SEnvir.Connections.Contains(this)) - throw new InvalidOperationException("Connection was not found in list"); - - SEnvir.Connections.Remove(this); - SEnvir.IPCount[IPAddress]--; - SEnvir.DBytesSent += TotalBytesSent; - SEnvir.DBytesReceived += TotalBytesReceived; + TcpServer.Disconnect(this); } public override void SendDisconnect(Packet p) @@ -164,7 +158,7 @@ public void CleanUp() Observed = null; // ItemList.Clear(); - // MagicList.Clear(); + // MagicList.Clear(); } public override void Process() @@ -179,25 +173,15 @@ public override void Process() if (TotalPacketsProcessed == 0 && TotalBytesReceived > 1024) { TryDisconnect(); - SEnvir.IPBlocks[IPAddress] = SEnvir.Now.Add(Config.PacketBanTime); - - for (int i = SEnvir.Connections.Count - 1; i >= 0; i--) - if (SEnvir.Connections[i].IPAddress == IPAddress) - SEnvir.Connections[i].TryDisconnect(); - + TcpServer.IpBan(IPAddress, Config.PacketBanTime); SEnvir.Log($"{IPAddress} Disconnected, Large Packet"); return; } - + if (ReceiveList.Count > Config.MaxPacket) { TryDisconnect(); - SEnvir.IPBlocks[IPAddress] = SEnvir.Now.Add(Config.PacketBanTime); - - for (int i = SEnvir.Connections.Count - 1; i >= 0; i--) - if (SEnvir.Connections[i].IPAddress == IPAddress) - SEnvir.Connections[i].TryDisconnect(); - + TcpServer.IpBan(IPAddress, Config.PacketBanTime); SEnvir.Log($"{IPAddress} Disconnected, Large amount of Packets"); return; } @@ -758,13 +742,13 @@ public void Process(C.RankSearch p) public void Process(C.ObserverRequest p) { - if (!Config.AllowObservation && (Account == null || (!Account.TempAdmin && !Account.Observer))) return; + if (!Config.AllowObservation && (Account == null || !Account.TempAdmin && !Account.Observer)) return; PlayerObject player = SEnvir.GetPlayerByCharacter(p.Name); if (player == null || player == Player) return; - if (!player.Character.Observable && (Account == null || (!Account.TempAdmin && !Account.Observer))) return; + if (!player.Character.Observable && (Account == null || !Account.TempAdmin && !Account.Observer)) return; if (Stage == GameStage.Game) Player.StopGame(); diff --git a/ServerLibrary/Infrastructure.Network/TcpServer.cs b/ServerLibrary/Infrastructure.Network/TcpServer.cs new file mode 100644 index 00000000..23e9ac42 --- /dev/null +++ b/ServerLibrary/Infrastructure.Network/TcpServer.cs @@ -0,0 +1,285 @@ +using Library; +using Library.Network; +using Server.DBModels; +using Server.Envir; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using G = Library.Network.GeneralPackets; + +namespace Server.Infrastructure.Network +{ + public class TcpServer + { + public static bool NetworkStarted { get; set; } + + public static Dictionary IPBlocks = new Dictionary(); //TODO: this should be supplied & encapsulated in a Service (i.e ConnectionService.CanConnect(IpAddress)) + + public static Dictionary IPCount = new Dictionary(); + + public static List Connections = new List(); + public static ConcurrentQueue NewConnections; + + private static TcpListener _listener, _userCountListener; + + public static long DBytesSent, DBytesReceived; + + public static long TotalBytesSent, TotalBytesReceived; + public static long PreviousTotalReceived, PreviousTotalSent; + + public static long DownloadSpeed, UploadSpeed; + + public static void StartNetwork(bool log = true) + { + try + { + NewConnections = new ConcurrentQueue(); + + _listener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.Port); + _listener.Start(); + _listener.BeginAcceptTcpClient(Connection, null); + + _userCountListener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.UserCountPort); + _userCountListener.Start(); + _userCountListener.BeginAcceptTcpClient(CountConnection, null); + + NetworkStarted = true; + if (log) SEnvir.Log($"Network Started. Listen: {Config.IPAddress}:{Config.Port}"); + } + catch (Exception ex) + { + NetworkStarted = false; + SEnvir.Log(ex.ToString()); + } + } + + public static void StopNetwork(bool log = true) + { + TcpListener expiredListener = _listener; + TcpListener expiredUserListener = _userCountListener; + + _listener = null; + _userCountListener = null; + + NetworkStarted = false; + + expiredListener?.Stop(); + expiredUserListener?.Stop(); + + NewConnections = null; + + try + { + Packet p = new G.Disconnect { Reason = DisconnectReason.ServerClosing }; + for (int i = Connections.Count - 1; i >= 0; i--) + Connections[i].SendDisconnect(p); + + Thread.Sleep(2000); + } + catch (Exception ex) + { + SEnvir.Log(ex.ToString()); + } + + if (log) SEnvir.Log("Network Stopped."); + } + + private static void Connection(IAsyncResult result) + { + try + { + if (_listener == null || !_listener.Server.IsBound) return; + + TcpClient client = _listener.EndAcceptTcpClient(result); + + string ipAddress = client.Client.RemoteEndPoint.ToString().Split(':')[0]; + + if (!IPBlocks.TryGetValue(ipAddress, out DateTime banDate) || banDate < SEnvir.Now) + { + SConnection Connection = new SConnection(client); + + if (Connection.Connected) + NewConnections?.Enqueue(Connection); + } + } + catch (SocketException) + { + + } + catch (Exception ex) + { + SEnvir.Log(ex.ToString()); + } + finally + { + while (NewConnections?.Count >= 15) + Thread.Sleep(1); + + if (_listener != null && _listener.Server.IsBound) + _listener.BeginAcceptTcpClient(Connection, null); + } + } + + private static void CountConnection(IAsyncResult result) + { + try + { + if (_userCountListener == null || !_userCountListener.Server.IsBound) return; + + TcpClient client = _userCountListener.EndAcceptTcpClient(result); + + byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", Connections.Count)); + + client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, CountConnectionEnd, client); + } + catch { } + finally + { + if (_userCountListener != null && _userCountListener.Server.IsBound) + _userCountListener.BeginAcceptTcpClient(CountConnection, null); + } + } + private static void CountConnectionEnd(IAsyncResult result) + { + try + { + TcpClient client = result.AsyncState as TcpClient; + + if (client == null) return; + + client.Client.EndSend(result); + + client.Client.Dispose(); + } + catch { } + } + + public static void Process() + { + SConnection connection; + while (!NewConnections.IsEmpty) + { + if (!NewConnections.TryDequeue(out connection)) break; + + IPCount.TryGetValue(connection.IPAddress, out var ipCount); + + IPCount[connection.IPAddress] = ipCount + 1; + + Connections.Add(connection); + } + + long bytesSent = 0; + long bytesReceived = 0; + + for (int i = Connections.Count - 1; i >= 0; i--) + { + if (i >= Connections.Count) break; + + connection = Connections[i]; + + connection.Process(); + bytesSent += connection.TotalBytesSent; + bytesReceived += connection.TotalBytesReceived; + } + + TotalBytesSent = DBytesSent + bytesSent; + TotalBytesReceived = DBytesReceived + bytesReceived; + + DownloadSpeed = TotalBytesReceived - PreviousTotalReceived; + UploadSpeed = TotalBytesSent - PreviousTotalSent; + + PreviousTotalReceived = TotalBytesReceived; + PreviousTotalSent = TotalBytesSent; + } + + //TODO: work out what SendDisconnect is doing differently to a Enqueue and see if we can consolodate. + internal static void Broadcast(Packet packet) + { + for (int i = Connections.Count - 1; i >= 0; i--) + Connections[i].SendDisconnect(packet); + } + + #region Not Connection Stuff + //TODO: pull out the chat logic from here - its not a network component + internal static void BroadcastOnlineMessage() + { + foreach (SConnection conn in Connections) + { + conn.ReceiveChat(string.Format(conn.Language.OnlineCount, Connections.Count(x => x.Stage == GameStage.Game), Connections.Count(x => x.Stage == GameStage.Observer)), MessageType.Hint); + + switch (conn.Stage) + { + case GameStage.Game: + if (conn.Player.Character.Observable) + conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observers.Count), MessageType.Hint); + break; + case GameStage.Observer: + conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observed.Observers.Count), MessageType.Hint); + break; + } + } + } + + internal static void BroadcastSystemMessage(Func messageSupplier) + { + foreach (SConnection con in Connections) + { + switch (con.Stage) + { + case GameStage.Game: + case GameStage.Observer: + con.ReceiveChat(messageSupplier.Invoke(con), MessageType.System); + break; + default: + continue; + } + } + } + + internal static void BroadcastMessage(string text, List linkedItems, MessageType messageType, Predicate shouldReceive) + { + foreach (SConnection con in Connections) + { + switch (con.Stage) + { + case GameStage.Game: + case GameStage.Observer: + if (!shouldReceive.Invoke(con)) continue; + + con.ReceiveChat(text, messageType, linkedItems); + break; + default: continue; + } + } + } + #endregion + + + internal static void IpBan(string ipAddress, TimeSpan duration) + { + IPBlocks[ipAddress] = SEnvir.Now.Add(duration); + + for (int i = Connections.Count - 1; i >= 0; i--) + if (Connections[i].IPAddress == ipAddress) + Connections[i].TryDisconnect(); + } + + internal static void Disconnect(SConnection connection) + { + if (!Connections.Contains(connection)) + throw new InvalidOperationException("Connection was not found in list"); //TODO: do we want to cause an exception here? Why not just log a warning and return? + + Connections.Remove(connection); + IPCount[connection.IPAddress]--; + DBytesSent += TotalBytesSent; + DBytesReceived += TotalBytesReceived; + } + + + } +} diff --git a/ServerLibrary/Models/ConquestWar.cs b/ServerLibrary/Models/ConquestWar.cs index 15837798..9ab38c5a 100644 --- a/ServerLibrary/Models/ConquestWar.cs +++ b/ServerLibrary/Models/ConquestWar.cs @@ -2,6 +2,7 @@ using Library.SystemModels; using Server.DBModels; using Server.Envir; +using Server.Infrastructure.Network; using Server.Models.Monsters; using System; using System.Collections.Generic; @@ -25,8 +26,7 @@ public sealed class ConquestWar public void StartWar() { - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestStarted, Castle.Name), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestStarted, Castle.Name)); Map = SEnvir.GetMap(Castle.Map); @@ -62,8 +62,7 @@ public void Process() public void EndWar() { - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestFinished, Castle.Name), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestFinished, Castle.Name)); Ended = true; @@ -88,8 +87,7 @@ public void EndWar() if (ownerGuild != null) { - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestOwner, ownerGuild.GuildName, Castle.Name), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestOwner, ownerGuild.GuildName, Castle.Name)); UserConquest conquest = SEnvir.UserConquestList.Binding.FirstOrDefault(x => x.Castle == Castle && x.Castle == ownerGuild?.Castle); diff --git a/ServerLibrary/Models/Map.cs b/ServerLibrary/Models/Map.cs index 22c9a239..e7f4d522 100644 --- a/ServerLibrary/Models/Map.cs +++ b/ServerLibrary/Models/Map.cs @@ -3,6 +3,7 @@ using Library.SystemModels; using Server.DBModels; using Server.Envir; +using Server.Infrastructure.Network; using Server.Models.Monsters; using System; using System.Collections.Generic; @@ -442,13 +443,11 @@ public void DoSpawn(bool eventSpawn) { if (Info.Delay >= 1000000) { - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat($"{mob.MonsterInfo.MonsterName} has appeared.", MessageType.System); + TcpServer.BroadcastMessage($"{mob.MonsterInfo.MonsterName} has appeared.", null, MessageType.System, c => true); } else { - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.BossSpawn, CurrentMap.Info.Description), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.BossSpawn, CurrentMap.Info.Description)); } } diff --git a/ServerLibrary/Models/Monsters/CastleFlag.cs b/ServerLibrary/Models/Monsters/CastleFlag.cs index b66c6917..5f612871 100644 --- a/ServerLibrary/Models/Monsters/CastleFlag.cs +++ b/ServerLibrary/Models/Monsters/CastleFlag.cs @@ -3,6 +3,7 @@ using Library.SystemModels; using Server.DBModels; using Server.Envir; +using Server.Infrastructure.Network; using System; using System.Drawing; using System.Linq; @@ -71,9 +72,7 @@ public override void Process() if (Target == null && Contester != null) { - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name), MessageType.System); - + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name)); Contester = null; ContesterTime = DateTime.MaxValue; } @@ -128,9 +127,7 @@ protected override void Attack() //Start 30 seconds timer ContesterTime = SEnvir.Now.Add(ContesterDelay); - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, _takeDuration), MessageType.System); - + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, _takeDuration)); return; } else @@ -154,9 +151,7 @@ protected override void Attack() { //Another guild near flag - reset contest time ContesterTime = SEnvir.Now.Add(ContesterDelay); - - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestPreventingFlag, player.Character.Account.GuildMember.Guild.GuildName, Contester.GuildName, War.Castle.Name), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestPreventingFlag, player.Character.Account.GuildMember.Guild.GuildName, Contester.GuildName, War.Castle.Name)); return; } @@ -168,8 +163,7 @@ protected override void Attack() if (!contestGuildNear) { - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name)); Contester = null; ContesterTime = DateTime.MaxValue; @@ -179,9 +173,7 @@ protected override void Attack() else { var difference = (ContesterTime - SEnvir.Now).Seconds; - - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, difference), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, difference)); } } @@ -196,8 +188,7 @@ protected override void Attack() //Update new guild with castle Contester.Castle = War.Castle; - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestCapture, Contester.GuildName, War.Castle.Name), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, Contester.GuildName, War.Castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = War.Castle.Index, Owner = Contester.GuildName }); diff --git a/ServerLibrary/Models/Monsters/CastleLord.cs b/ServerLibrary/Models/Monsters/CastleLord.cs index 19220ede..e35551e7 100644 --- a/ServerLibrary/Models/Monsters/CastleLord.cs +++ b/ServerLibrary/Models/Monsters/CastleLord.cs @@ -1,6 +1,7 @@ using Library; using Server.DBModels; using Server.Envir; +using Server.Infrastructure.Network; using System; using System.Collections.Generic; using System.Drawing; @@ -280,9 +281,7 @@ public override void Die() EXPOwner.Character.Account.GuildMember.Guild.Castle = War.Castle; - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.ConquestCapture, EXPOwner.Character.Account.GuildMember.Guild.GuildName, War.Castle.Name), MessageType.System); - + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, EXPOwner.Character.Account.GuildMember.Guild.GuildName, War.Castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = War.Castle.Index, Owner = EXPOwner.Character.Account.GuildMember.Guild.GuildName }); War.CastleTarget = null; diff --git a/ServerLibrary/Models/Monsters/JinamStoneGate.cs b/ServerLibrary/Models/Monsters/JinamStoneGate.cs index 012e2df6..6c11c4a9 100644 --- a/ServerLibrary/Models/Monsters/JinamStoneGate.cs +++ b/ServerLibrary/Models/Monsters/JinamStoneGate.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Library; using Server.Envir; +using Server.Infrastructure.Network; namespace Server.Models.Monsters { @@ -34,8 +35,7 @@ protected override void OnSpawned() DespawnTime = SEnvir.Now.AddMinutes(20); - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.LairGateOpen, CurrentMap.Info.Description, CurrentLocation), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.LairGateOpen, CurrentMap.Info.Description, CurrentLocation)); } @@ -48,8 +48,7 @@ public override void Process() if (SpawnInfo != null) SpawnInfo.AliveCount--; - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(con.Language.LairGateClosed, MessageType.System); + TcpServer.BroadcastSystemMessage(c => c.Language.LairGateClosed); SpawnInfo = null; Despawn(); diff --git a/ServerLibrary/Models/Monsters/NetherworldGate.cs b/ServerLibrary/Models/Monsters/NetherworldGate.cs index 4c8f5ad5..d43ab944 100644 --- a/ServerLibrary/Models/Monsters/NetherworldGate.cs +++ b/ServerLibrary/Models/Monsters/NetherworldGate.cs @@ -2,6 +2,7 @@ using System.Drawing; using Library; using Server.Envir; +using Server.Infrastructure.Network; namespace Server.Models.Monsters { @@ -28,9 +29,7 @@ protected override void OnSpawned() base.OnSpawned(); DespawnTime = SEnvir.Now.AddMinutes(20); - - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(string.Format(con.Language.NetherGateOpen, CurrentMap.Info.Description, CurrentLocation), MessageType.System); + TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.NetherGateOpen, CurrentMap.Info.Description, CurrentLocation)); } public override void Process() @@ -42,8 +41,7 @@ public override void Process() if (SpawnInfo != null) SpawnInfo.AliveCount--; - foreach (SConnection con in SEnvir.Connections) - con.ReceiveChat(con.Language.NetherGateClosed, MessageType.System); + TcpServer.BroadcastSystemMessage(c => c.Language.NetherGateClosed); SpawnInfo = null; Despawn(); diff --git a/ServerLibrary/Models/PlayerObject.cs b/ServerLibrary/Models/PlayerObject.cs index 868407c4..9ad20ddf 100644 --- a/ServerLibrary/Models/PlayerObject.cs +++ b/ServerLibrary/Models/PlayerObject.cs @@ -5,6 +5,7 @@ using Server.DBModels; using Server.Envir; using Server.Envir.Events.Triggers; +using Server.Infrastructure.Network; using Server.Models.Magics; using Server.Models.Monsters; using System; @@ -1569,20 +1570,8 @@ public void Chat(string text) } text = string.Format("(!@){0}: {1}", Name, text.Remove(0, 2)); + TcpServer.BroadcastMessage(text, linkedItems, MessageType.Global, c => !SEnvir.IsBlocking(Character.Account, c.Account)); - foreach (SConnection con in SEnvir.Connections) - { - switch (con.Stage) - { - case GameStage.Game: - case GameStage.Observer: - if (SEnvir.IsBlocking(Character.Account, con.Account)) continue; - - con.ReceiveChat(text, MessageType.Global, linkedItems); - break; - default: continue; - } - } } else if (text.StartsWith("!")) { @@ -1624,18 +1613,7 @@ public void Chat(string text) if (!Character.Account.TempAdmin) return; text = string.Format("{0}: {1}", Name, text.Remove(0, 2)); - - foreach (SConnection con in SEnvir.Connections) - { - switch (con.Stage) - { - case GameStage.Game: - case GameStage.Observer: - con.ReceiveChat(text, MessageType.Announcement, linkedItems); - break; - default: continue; - } - } + TcpServer.BroadcastMessage(text, linkedItems, MessageType.Announcement, c => true); } else if (text.StartsWith("@")) { @@ -1760,38 +1738,14 @@ public void ObserverChat(SConnection con, string text) } text = string.Format("(!@){0}: {1}", con.Account.LastCharacter.CharacterName, text.Remove(0, 2)); - - foreach (SConnection target in SEnvir.Connections) - { - switch (target.Stage) - { - case GameStage.Game: - case GameStage.Observer: - if (SEnvir.IsBlocking(con.Account, target.Account)) continue; - - target.ReceiveChat(text, MessageType.Global); - break; - default: continue; - } - } + TcpServer.BroadcastMessage(text, null, MessageType.Global, c => SEnvir.IsBlocking(con.Account, c.Account)); } else if (text.StartsWith("@!")) { if (!con.Account.LastCharacter.Account.TempAdmin) return; text = string.Format("{0}: {1}", con.Account.LastCharacter.CharacterName, text.Remove(0, 2)); - - foreach (SConnection target in SEnvir.Connections) - { - switch (target.Stage) - { - case GameStage.Game: - case GameStage.Observer: - target.ReceiveChat(text, MessageType.Announcement); - break; - default: continue; - } - } + TcpServer.BroadcastMessage(text, null, MessageType.Announcement, c => true); } else { @@ -6034,18 +5988,7 @@ public void ItemUse(CellLinkInfo link) string text = $"A [{item.Info.ItemName}] has been used in {CurrentMap.Info.Description}"; - - foreach (SConnection con in SEnvir.Connections) - { - switch (con.Stage) - { - case GameStage.Game: - case GameStage.Observer: - con.ReceiveChat(text, MessageType.System); - break; - default: continue; - } - } + TcpServer.BroadcastMessage(text, null, MessageType.System, c => true); } } } From 12f7d99c3f8cc52ae6ab3edf0ae31898a65f23e2 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 3 Aug 2025 14:09:57 +0100 Subject: [PATCH 2/6] Adding IpService to encapsulate IpBans --- ServerCore/Program.cs | 3 +- .../Commands/Command/Admin/ClearIPBlocks.cs | 9 +- ServerLibrary/Envir/SEnvir.cs | 11 +- .../Infrastructure.Network/IpService.cs | 40 +++ .../Infrastructure.Network/SConnection.cs | 17 -- .../Infrastructure.Network/TcpServer.cs | 253 +++++++++--------- 6 files changed, 183 insertions(+), 150 deletions(-) create mode 100644 ServerLibrary/Infrastructure.Network/IpService.cs diff --git a/ServerCore/Program.cs b/ServerCore/Program.cs index f1681abc..2ded1326 100644 --- a/ServerCore/Program.cs +++ b/ServerCore/Program.cs @@ -1,5 +1,4 @@ -using Autofac; -using Library; +using Library; using Server.Envir; using System; using System.Reflection; diff --git a/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs b/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs index 9fab7db1..8303d784 100644 --- a/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs +++ b/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs @@ -7,9 +7,16 @@ class ClearIPBlocks : AbstractCommand { public override string VALUE => "CLEARIPBLOCKS"; + + //private readonly IpService IpBanService; + //public ClearIPBlocks(IpService ipBanService) { + // IpBanService = ipBanService; + //} + public override void Action(PlayerObject player) { - TcpServer.IPBlocks.Clear(); + SEnvir.IpService.Reset(); //TODO: IpBanService should be injected but need to rewrite everything to use DI (AutoFac) + //because reflection based creation is not flexible enough } } } diff --git a/ServerLibrary/Envir/SEnvir.cs b/ServerLibrary/Envir/SEnvir.cs index 1e4ae435..8042727e 100644 --- a/ServerLibrary/Envir/SEnvir.cs +++ b/ServerLibrary/Envir/SEnvir.cs @@ -71,13 +71,10 @@ public static void LogChat(string log) #endregion - #region Network + public static readonly IpService IpService = new IpService(); + private static readonly TcpServer TcpServer = new TcpServer(IpService); - - - #endregion - - //TODO: make this public readonly - too much leakage, expose a SEnvir.Stop() method where we are tryingto do SEnvir.Started = false; + //TODO: make this public readonly - too much leakage, expose a SEnvir.Stop() method where we are trying to do SEnvir.Started = false instead; public static bool Started { get => TcpServer.NetworkStarted; @@ -2789,7 +2786,7 @@ public static void NewAccount(C.NewAccount p, SConnection con) if (nowcount > 2 || todaycount > 5) { - TcpServer.IpBan(con.IPAddress, TimeSpan.FromDays(7)); + IpService.Ban(con, TimeSpan.FromDays(7)); Log($"{con.IPAddress} Disconnected and banned for trying too many accounts"); return; } diff --git a/ServerLibrary/Infrastructure.Network/IpService.cs b/ServerLibrary/Infrastructure.Network/IpService.cs new file mode 100644 index 00000000..11d458c5 --- /dev/null +++ b/ServerLibrary/Infrastructure.Network/IpService.cs @@ -0,0 +1,40 @@ +using Server.Envir; +using System; +using System.Collections.Generic; +using System.Net; + +namespace Server.Infrastructure.Network +{ + public class IpService + { + public static Dictionary IPBlocks = new Dictionary(); //TODO: this should be supplied & encapsulated in a Service (i.e ConnectionService.CanConnect(IpAddress)) + + public bool IsBanned(SConnection connection) + { + return IsBanned(connection.IPAddress); + } + + public bool IsBanned(string ipAddress) + { + return IPBlocks.TryGetValue(ipAddress, out DateTime bannedUntil) && bannedUntil > SEnvir.Now; + } + + public void Ban(SConnection connection, TimeSpan duration) + { + var bannedUntil = DateTime.Now.Add(duration); + if (!IPBlocks.TryAdd(connection.IPAddress, bannedUntil)) + IPBlocks[connection.IPAddress] = bannedUntil; + connection.TryDisconnect(); + } + + public void Ban(SConnection connection) + { + Ban(connection, TimeSpan.MaxValue); + } + + public void Reset() + { + IPBlocks.Clear(); + } + } +} diff --git a/ServerLibrary/Infrastructure.Network/SConnection.cs b/ServerLibrary/Infrastructure.Network/SConnection.cs index a811544d..fd4a9bb2 100644 --- a/ServerLibrary/Infrastructure.Network/SConnection.cs +++ b/ServerLibrary/Infrastructure.Network/SConnection.cs @@ -169,23 +169,6 @@ public override void Process() PingSent = true; Enqueue(new G.Ping { ObserverPacket = false }); } - - if (TotalPacketsProcessed == 0 && TotalBytesReceived > 1024) - { - TryDisconnect(); - TcpServer.IpBan(IPAddress, Config.PacketBanTime); - SEnvir.Log($"{IPAddress} Disconnected, Large Packet"); - return; - } - - if (ReceiveList.Count > Config.MaxPacket) - { - TryDisconnect(); - TcpServer.IpBan(IPAddress, Config.PacketBanTime); - SEnvir.Log($"{IPAddress} Disconnected, Large amount of Packets"); - return; - } - base.Process(); } diff --git a/ServerLibrary/Infrastructure.Network/TcpServer.cs b/ServerLibrary/Infrastructure.Network/TcpServer.cs index 23e9ac42..c92d07b9 100644 --- a/ServerLibrary/Infrastructure.Network/TcpServer.cs +++ b/ServerLibrary/Infrastructure.Network/TcpServer.cs @@ -18,9 +18,8 @@ public class TcpServer { public static bool NetworkStarted { get; set; } - public static Dictionary IPBlocks = new Dictionary(); //TODO: this should be supplied & encapsulated in a Service (i.e ConnectionService.CanConnect(IpAddress)) - - public static Dictionary IPCount = new Dictionary(); + public readonly IpService IpService; + //public static Dictionary IPCount = new Dictionary(); //TODO: this isn't used but i imagine its intended to limit connections from single IP public static List Connections = new List(); public static ConcurrentQueue NewConnections; @@ -34,7 +33,12 @@ public class TcpServer public static long DownloadSpeed, UploadSpeed; - public static void StartNetwork(bool log = true) + public TcpServer(IpService ipBanService) + { + IpService = ipBanService; + } + + public void StartNetwork(bool log = true) { try { @@ -58,117 +62,15 @@ public static void StartNetwork(bool log = true) } } - public static void StopNetwork(bool log = true) - { - TcpListener expiredListener = _listener; - TcpListener expiredUserListener = _userCountListener; - - _listener = null; - _userCountListener = null; - - NetworkStarted = false; - - expiredListener?.Stop(); - expiredUserListener?.Stop(); - - NewConnections = null; - - try - { - Packet p = new G.Disconnect { Reason = DisconnectReason.ServerClosing }; - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(p); - - Thread.Sleep(2000); - } - catch (Exception ex) - { - SEnvir.Log(ex.ToString()); - } - - if (log) SEnvir.Log("Network Stopped."); - } - - private static void Connection(IAsyncResult result) - { - try - { - if (_listener == null || !_listener.Server.IsBound) return; - - TcpClient client = _listener.EndAcceptTcpClient(result); - - string ipAddress = client.Client.RemoteEndPoint.ToString().Split(':')[0]; - - if (!IPBlocks.TryGetValue(ipAddress, out DateTime banDate) || banDate < SEnvir.Now) - { - SConnection Connection = new SConnection(client); - - if (Connection.Connected) - NewConnections?.Enqueue(Connection); - } - } - catch (SocketException) - { - - } - catch (Exception ex) - { - SEnvir.Log(ex.ToString()); - } - finally - { - while (NewConnections?.Count >= 15) - Thread.Sleep(1); - - if (_listener != null && _listener.Server.IsBound) - _listener.BeginAcceptTcpClient(Connection, null); - } - } - - private static void CountConnection(IAsyncResult result) - { - try - { - if (_userCountListener == null || !_userCountListener.Server.IsBound) return; - - TcpClient client = _userCountListener.EndAcceptTcpClient(result); - - byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", Connections.Count)); - - client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, CountConnectionEnd, client); - } - catch { } - finally - { - if (_userCountListener != null && _userCountListener.Server.IsBound) - _userCountListener.BeginAcceptTcpClient(CountConnection, null); - } - } - private static void CountConnectionEnd(IAsyncResult result) - { - try - { - TcpClient client = result.AsyncState as TcpClient; - - if (client == null) return; - - client.Client.EndSend(result); - - client.Client.Dispose(); - } - catch { } - } - - public static void Process() + public void Process() { SConnection connection; while (!NewConnections.IsEmpty) { if (!NewConnections.TryDequeue(out connection)) break; - IPCount.TryGetValue(connection.IPAddress, out var ipCount); - - IPCount[connection.IPAddress] = ipCount + 1; + //IPCount.TryGetValue(connection.IPAddress, out var ipCount); + //IPCount[connection.IPAddress] = ipCount + 1; Connections.Add(connection); } @@ -181,8 +83,24 @@ public static void Process() if (i >= Connections.Count) break; connection = Connections[i]; - connection.Process(); + + if (connection.TotalPacketsProcessed == 0 && connection.TotalBytesReceived > 1024) + { + connection.TryDisconnect(); + IpService.Ban(connection, Config.PacketBanTime); + SEnvir.Log($"{connection.IPAddress} Disconnected, Large Packet"); + return; + } + + if (connection.ReceiveList.Count > Config.MaxPacket) + { + connection.TryDisconnect(); + IpService.Ban(connection, Config.PacketBanTime); + SEnvir.Log($"{connection.IPAddress} Disconnected, Large amount of Packets"); + return; + } + bytesSent += connection.TotalBytesSent; bytesReceived += connection.TotalBytesReceived; } @@ -197,14 +115,45 @@ public static void Process() PreviousTotalSent = TotalBytesSent; } + public void StopNetwork(bool log = true) + { + TcpListener expiredListener = _listener; + TcpListener expiredUserListener = _userCountListener; + + _listener = null; + _userCountListener = null; + + NetworkStarted = false; + + expiredListener?.Stop(); + expiredUserListener?.Stop(); + + NewConnections = null; + + try + { + Packet p = new G.Disconnect { Reason = DisconnectReason.ServerClosing }; + for (int i = Connections.Count - 1; i >= 0; i--) + Connections[i].SendDisconnect(p); + + Thread.Sleep(2000); + } + catch (Exception ex) + { + SEnvir.Log(ex.ToString()); + } + + if (log) SEnvir.Log("Network Stopped."); + } + //TODO: work out what SendDisconnect is doing differently to a Enqueue and see if we can consolodate. - internal static void Broadcast(Packet packet) + public void Broadcast(Packet packet) { for (int i = Connections.Count - 1; i >= 0; i--) Connections[i].SendDisconnect(packet); } - #region Not Connection Stuff + #region GameEngine Logic Stuff - Move Out //TODO: pull out the chat logic from here - its not a network component internal static void BroadcastOnlineMessage() { @@ -259,27 +208,85 @@ internal static void BroadcastMessage(string text, List linkedIt } #endregion - - internal static void IpBan(string ipAddress, TimeSpan duration) - { - IPBlocks[ipAddress] = SEnvir.Now.Add(duration); - - for (int i = Connections.Count - 1; i >= 0; i--) - if (Connections[i].IPAddress == ipAddress) - Connections[i].TryDisconnect(); - } - internal static void Disconnect(SConnection connection) { if (!Connections.Contains(connection)) throw new InvalidOperationException("Connection was not found in list"); //TODO: do we want to cause an exception here? Why not just log a warning and return? Connections.Remove(connection); - IPCount[connection.IPAddress]--; + //IPCount[connection.IPAddress]--; DBytesSent += TotalBytesSent; DBytesReceived += TotalBytesReceived; } + #region Connection Stuff - Move out + private void Connection(IAsyncResult result) + { + try + { + if (_listener == null || !_listener.Server.IsBound) return; + + TcpClient client = _listener.EndAcceptTcpClient(result); + + string ipAddress = client.Client.RemoteEndPoint.ToString().Split(':')[0]; + if (IpService.IsBanned(ipAddress)) + { + SConnection Connection = new SConnection(client); + + if (Connection.Connected) + NewConnections?.Enqueue(Connection); + } + } + catch (SocketException) + { + + } + catch (Exception ex) + { + SEnvir.Log(ex.ToString()); + } + finally + { + while (NewConnections?.Count >= 15) + Thread.Sleep(1); + + if (_listener != null && _listener.Server.IsBound) + _listener.BeginAcceptTcpClient(Connection, null); + } + } + private void CountConnection(IAsyncResult result) + { + try + { + if (_userCountListener == null || !_userCountListener.Server.IsBound) return; + + TcpClient client = _userCountListener.EndAcceptTcpClient(result); + + byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", Connections.Count)); + + client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, CountConnectionEnd, client); + } + catch { } + finally + { + if (_userCountListener != null && _userCountListener.Server.IsBound) + _userCountListener.BeginAcceptTcpClient(CountConnection, null); + } + } + private void CountConnectionEnd(IAsyncResult result) + { + try + { + TcpClient client = result.AsyncState as TcpClient; + + if (client == null) return; + client.Client.EndSend(result); + + client.Client.Dispose(); + } + catch { } + } + #endregion } } From 2c712a11b6bc112ad7f8de3830ad29fd199f651f Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 4 Aug 2025 00:29:55 +0100 Subject: [PATCH 3/6] Moving internal SConnection infrastructure and logic out of tcp server into its own isolated components. --- LibraryCore/Network/BaseConnection.cs | 2 + Server/SMain.cs | 63 +++-- ServerLibrary/Envir/BroadcastService.cs | 65 ++++++ .../Commands/Command/Admin/ClearIPBlocks.cs | 9 +- .../Commands/Command/Admin/TakeCastle.cs | 5 +- ServerLibrary/Envir/SEnvir.cs | 22 +- .../ConnectionFactory.cs | 10 + .../ConnectionManager.cs | 131 +++++++++++ .../IpAddressManager.cs | 30 +++ .../Infrastructure.Network/IpService.cs | 40 ---- .../Infrastructure.Network/SConnection.cs | 13 +- .../SConnectionFactory.cs | 15 ++ .../SConnectionManager.cs | 12 + .../Infrastructure.Network/TcpServer.cs | 215 ++---------------- ServerLibrary/Models/ConquestWar.cs | 9 +- ServerLibrary/Models/Map.cs | 4 +- ServerLibrary/Models/Monsters/CastleFlag.cs | 14 +- ServerLibrary/Models/Monsters/CastleLord.cs | 3 +- .../Models/Monsters/JinamStoneGate.cs | 10 +- .../Models/Monsters/NetherworldGate.cs | 5 +- ServerLibrary/Models/PlayerObject.cs | 10 +- 21 files changed, 365 insertions(+), 322 deletions(-) create mode 100644 ServerLibrary/Envir/BroadcastService.cs create mode 100644 ServerLibrary/Infrastructure.Network/ConnectionFactory.cs create mode 100644 ServerLibrary/Infrastructure.Network/ConnectionManager.cs create mode 100644 ServerLibrary/Infrastructure.Network/IpAddressManager.cs delete mode 100644 ServerLibrary/Infrastructure.Network/IpService.cs create mode 100644 ServerLibrary/Infrastructure.Network/SConnectionFactory.cs create mode 100644 ServerLibrary/Infrastructure.Network/SConnectionManager.cs diff --git a/LibraryCore/Network/BaseConnection.cs b/LibraryCore/Network/BaseConnection.cs index 29a8dcfa..3525f642 100644 --- a/LibraryCore/Network/BaseConnection.cs +++ b/LibraryCore/Network/BaseConnection.cs @@ -22,6 +22,8 @@ public abstract class BaseConnection public bool AdditionalLogging; + public string IPAddress => Client?.Client?.RemoteEndPoint?.ToString()?.Split(':')[0]; + protected TcpClient Client; public DateTime TimeConnected { get; set; } diff --git a/Server/SMain.cs b/Server/SMain.cs index 6f32b1b2..5d52d5a4 100644 --- a/Server/SMain.cs +++ b/Server/SMain.cs @@ -8,7 +8,6 @@ using PluginCore; using Server.DBModels; using Server.Envir; -using Server.Infrastructure.Network; //TODO: this should really be exposed here... maybe create a ServerMetric class to share between the two. using Server.Views; using System; using System.Collections.Generic; @@ -189,7 +188,7 @@ private void UpdateInterface() StartServerButton.Enabled = SEnvir.EnvirThread == null; StopServerButton.Enabled = SEnvir.Started; - ConnectionLabel.Caption = string.Format(@"Connections: {0:#,##0}", TcpServer.Connections.Count); + ConnectionLabel.Caption = string.Format(@"Connections: {0:#,##0}", SEnvir.SConnectionManager.Connections.Count); ObjectLabel.Caption = string.Format(@"Objects: {0} of {1:#,##0}", SEnvir.ActiveObjects.Count, SEnvir.Objects.Count); ProcessLabel.Caption = string.Format(@"Process Count: {0:#,##0}", SEnvir.ProcessObjectCount); LoopLabel.Caption = string.Format(@"Loop Count: {0:#,##0}", SEnvir.LoopCount); @@ -202,42 +201,42 @@ private void UpdateInterface() const decimal MB = KB * 1024; const decimal GB = MB * 1024; - if (TcpServer.TotalBytesReceived > GB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}GB", TcpServer.TotalBytesReceived / GB); - else if (TcpServer.TotalBytesReceived > MB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}MB", TcpServer.TotalBytesReceived / MB); - else if (TcpServer.TotalBytesReceived > KB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}KB", TcpServer.TotalBytesReceived / KB); + if (SEnvir.SConnectionManager.TotalBytesReceived > GB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}GB", SEnvir.SConnectionManager.TotalBytesReceived / GB); + else if (SEnvir.SConnectionManager.TotalBytesReceived > MB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}MB", SEnvir.SConnectionManager.TotalBytesReceived / MB); + else if (SEnvir.SConnectionManager.TotalBytesReceived > KB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}KB", SEnvir.SConnectionManager.TotalBytesReceived / KB); else - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}B", TcpServer.TotalBytesReceived); - - if (TcpServer.TotalBytesSent > GB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}GB", TcpServer.TotalBytesSent / GB); - else if (TcpServer.TotalBytesSent > MB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}MB", TcpServer.TotalBytesSent / MB); - else if (TcpServer.TotalBytesSent > KB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}KB", TcpServer.TotalBytesSent / KB); + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}B", SEnvir.SConnectionManager.TotalBytesReceived); + + if (SEnvir.SConnectionManager.TotalBytesSent > GB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}GB", SEnvir.SConnectionManager.TotalBytesSent / GB); + else if (SEnvir.SConnectionManager.TotalBytesSent > MB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}MB", SEnvir.SConnectionManager.TotalBytesSent / MB); + else if (SEnvir.SConnectionManager.TotalBytesSent > KB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}KB", SEnvir.SConnectionManager.TotalBytesSent / KB); else - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}B", TcpServer.TotalBytesSent); + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}B", SEnvir.SConnectionManager.TotalBytesSent); - if (TcpServer.DownloadSpeed > GB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}GBps", TcpServer.DownloadSpeed / GB); - else if (TcpServer.DownloadSpeed > MB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}MBps", TcpServer.DownloadSpeed / MB); - else if (TcpServer.DownloadSpeed > KB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}KBps", TcpServer.DownloadSpeed / KB); + if (SEnvir.TcpServer.DownloadSpeed > GB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}GBps", SEnvir.TcpServer.DownloadSpeed / GB); + else if (SEnvir.TcpServer.DownloadSpeed > MB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}MBps", SEnvir.TcpServer.DownloadSpeed / MB); + else if (SEnvir.TcpServer.DownloadSpeed > KB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}KBps", SEnvir.TcpServer.DownloadSpeed / KB); else - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}Bps", TcpServer.DownloadSpeed); - - if (TcpServer.UploadSpeed > GB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}GBps", TcpServer.UploadSpeed / GB); - else if (TcpServer.UploadSpeed > MB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}MBps", TcpServer.UploadSpeed / MB); - else if (TcpServer.UploadSpeed > KB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}KBps", TcpServer.UploadSpeed / KB); + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}Bps", SEnvir.TcpServer.DownloadSpeed); + + if (SEnvir.TcpServer.UploadSpeed > GB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}GBps", SEnvir.TcpServer.UploadSpeed / GB); + else if (SEnvir.TcpServer.UploadSpeed > MB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}MBps", SEnvir.TcpServer.UploadSpeed / MB); + else if (SEnvir.TcpServer.UploadSpeed > KB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}KBps", SEnvir.TcpServer.UploadSpeed / KB); else - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}Bps", TcpServer.UploadSpeed); + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}Bps", SEnvir.TcpServer.UploadSpeed); } private void StartServerButton_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) diff --git a/ServerLibrary/Envir/BroadcastService.cs b/ServerLibrary/Envir/BroadcastService.cs new file mode 100644 index 00000000..3f1108f4 --- /dev/null +++ b/ServerLibrary/Envir/BroadcastService.cs @@ -0,0 +1,65 @@ +using Library; +using Server.Infrastructure.Network; +using System; +using System.Collections.Generic; + +namespace Server.Envir +{ + public class BroadcastService(SConnectionManager ConnectionManager) + { + //TODO: dont think this should be here + internal void SendOnlineCount() + { + foreach (SConnection conn in ConnectionManager.Connections) + { + conn.ReceiveChat(string.Format(conn.Language.OnlineCount, ConnectionManager.Players, ConnectionManager.Observers), MessageType.Hint); + + switch (conn.Stage) + { + case GameStage.Game: + if (conn.Player.Character.Observable) + conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observers.Count), MessageType.Hint); + break; + case GameStage.Observer: + conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observed.Observers.Count), MessageType.Hint); + break; + } + } + } + + //TODO: dont think this should be here - this is also so convoluted because server owns the languages + internal void BroadcastSystemMessage(Func messageSupplier) + { + foreach (SConnection con in ConnectionManager.Connections) + { + switch (con.Stage) + { + case GameStage.Game: + case GameStage.Observer: + con.ReceiveChat(messageSupplier.Invoke(con), MessageType.System); + break; + default: + continue; + } + } + } + + //TODO: dont think this should be here + internal void BroadcastMessage(string text, List linkedItems, MessageType messageType, Predicate shouldReceive) + { + foreach (SConnection con in ConnectionManager.Connections) + { + switch (con.Stage) + { + case GameStage.Game: + case GameStage.Observer: + if (!shouldReceive.Invoke(con)) continue; + + con.ReceiveChat(text, messageType, linkedItems); + break; + default: continue; + } + } + } + } +} diff --git a/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs b/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs index 8303d784..7b536f0a 100644 --- a/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs +++ b/ServerLibrary/Envir/Commands/Command/Admin/ClearIPBlocks.cs @@ -7,16 +7,9 @@ class ClearIPBlocks : AbstractCommand { public override string VALUE => "CLEARIPBLOCKS"; - - //private readonly IpService IpBanService; - //public ClearIPBlocks(IpService ipBanService) { - // IpBanService = ipBanService; - //} - public override void Action(PlayerObject player) { - SEnvir.IpService.Reset(); //TODO: IpBanService should be injected but need to rewrite everything to use DI (AutoFac) - //because reflection based creation is not flexible enough + SEnvir.IpManager.Reset(); //TODO: this should be injected but need to rewrite everything to use DI (AutoFac) because reflection based creation is not flexible enough } } } diff --git a/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs b/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs index 8739ec4a..4a6b9fc6 100644 --- a/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs +++ b/ServerLibrary/Envir/Commands/Command/Admin/TakeCastle.cs @@ -31,7 +31,7 @@ public override void Action(PlayerObject player, string[] vals) throw new UserCommandException(string.Format("No guild currently owns {0} castle.", castle.Name)); ownerGuild.Castle = null; - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestLost, ownerGuild.GuildName, castle.Name)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestLost, ownerGuild.GuildName, castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = castle.Index, Owner = string.Empty }); foreach (PlayerObject user in SEnvir.Players) @@ -42,8 +42,7 @@ public override void Action(PlayerObject player, string[] vals) else { player.Character.Account.GuildMember.Guild.Castle = castle; - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, player.Character.Account.GuildMember.Guild.GuildName, castle.Name)); - + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, player.Character.Account.GuildMember.Guild.GuildName, castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = castle.Index, Owner = player.Character.Account.GuildMember.Guild.GuildName }); foreach (PlayerObject user in SEnvir.Players) user.ApplyCastleBuff(); diff --git a/ServerLibrary/Envir/SEnvir.cs b/ServerLibrary/Envir/SEnvir.cs index 8042727e..58002801 100644 --- a/ServerLibrary/Envir/SEnvir.cs +++ b/ServerLibrary/Envir/SEnvir.cs @@ -16,11 +16,8 @@ using System.Globalization; using System.IO; using System.Linq; -using System.Net; -using System.Net.Sockets; using System.Reflection; using System.Security.Cryptography; -using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; @@ -71,23 +68,26 @@ public static void LogChat(string log) #endregion - public static readonly IpService IpService = new IpService(); - private static readonly TcpServer TcpServer = new TcpServer(IpService); + public static readonly IpAddressManager IpManager = new IpAddressManager(); + public static readonly SConnectionManager SConnectionManager = new SConnectionManager( + new SConnectionFactory(() => SConnectionManager.RemoveConnection), + IpManager + ); + public static readonly TcpServer TcpServer = new TcpServer(SConnectionManager); + public static BroadcastService BroadcastService = new BroadcastService(SConnectionManager); - //TODO: make this public readonly - too much leakage, expose a SEnvir.Stop() method where we are trying to do SEnvir.Started = false instead; public static bool Started { - get => TcpServer.NetworkStarted; + get => TcpServer.Started; set { - if (TcpServer.NetworkStarted == value) return; + if (TcpServer.Started == value) return; if (value) TcpServer.StartNetwork(); else TcpServer.StopNetwork(); } } public static bool Saving { get; private set; } - //TODO: why are we leaking thread implementation here - why not just reuse !SEnvir.Started? logic here? public static Thread EnvirThread { get; private set; } public static DateTime Now, StartTime, LastWarTime; @@ -999,7 +999,7 @@ public static void EnvirLoop() if (Now >= UserCountTime) { UserCountTime = Now.AddMinutes(5); - TcpServer.BroadcastOnlineMessage(); + BroadcastService.SendOnlineCount(); } if (Now >= EventTimerTime) @@ -2786,7 +2786,7 @@ public static void NewAccount(C.NewAccount p, SConnection con) if (nowcount > 2 || todaycount > 5) { - IpService.Ban(con, TimeSpan.FromDays(7)); + IpManager.Timeout(con, TimeSpan.FromDays(7)); Log($"{con.IPAddress} Disconnected and banned for trying too many accounts"); return; } diff --git a/ServerLibrary/Infrastructure.Network/ConnectionFactory.cs b/ServerLibrary/Infrastructure.Network/ConnectionFactory.cs new file mode 100644 index 00000000..4e643006 --- /dev/null +++ b/ServerLibrary/Infrastructure.Network/ConnectionFactory.cs @@ -0,0 +1,10 @@ +using Library.Network; +using System.Net.Sockets; + +namespace Server.Infrastructure.Network +{ + public interface IConnectionFactory where ConnectionType : BaseConnection + { + public ConnectionType Create(TcpClient tcpClient); + } +} diff --git a/ServerLibrary/Infrastructure.Network/ConnectionManager.cs b/ServerLibrary/Infrastructure.Network/ConnectionManager.cs new file mode 100644 index 00000000..2d720ce0 --- /dev/null +++ b/ServerLibrary/Infrastructure.Network/ConnectionManager.cs @@ -0,0 +1,131 @@ +using Library; +using Library.Network; +using Server.Envir; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Threading; + +namespace Server.Infrastructure.Network +{ + public class ConnectionManager(IConnectionFactory ConnectionFactory, IpAddressManager IpManager) where ConnectionType : BaseConnection + { + //public static Dictionary IPCount = new Dictionary(); //TODO: this isn't used but i imagine its intended to limit connections from single IP + + private bool IsResetting = false; + + public ConcurrentQueue PendingConnections = []; + public List Connections = []; + + public bool AcceptingConnections => PendingConnections?.Count >= 15 || IsResetting; + + #region Metadata + public long DBytesSent, DBytesReceived; //TODO: not sure why these DBytesX exist? They get assigned to TotalBytesX but only get incremented when someone disconnects (incremented by total of TotalBytesX) + + public long TotalBytesSent, TotalBytesReceived; + public long PreviousTotalReceived, PreviousTotalSent; + #endregion + + internal void Add(TcpClient tcpClient) + { + string ipAddress = tcpClient.Client.RemoteEndPoint.ToString().Split(':')[0]; //TODO: this is duplicated inside BaseConnection - create a utility method + if (IpManager.IsAllowing(ipAddress)) + { + var connection = ConnectionFactory.Create(tcpClient); + if (connection.Connected) PendingConnections?.Enqueue(connection); + } + } + + internal void Process() + { + while (!PendingConnections.IsEmpty) + { + if (!PendingConnections.TryDequeue(out var connection)) break; + + //IPCount.TryGetValue(connection.IPAddress, out var ipCount); + //IPCount[connection.IPAddress] = ipCount + 1; + Connections.Add(connection); + } + + long bytesSent = 0; + long bytesReceived = 0; + for (int i = Connections.Count - 1; i >= 0; i--) + { + if (i >= Connections.Count) break; + + var connection = Connections[i]; + connection.Process(); + + if (connection.TotalPacketsProcessed == 0 && connection.TotalBytesReceived > 1024) + { + connection.TryDisconnect(); + IpManager.Timeout(connection, Config.PacketBanTime); + SEnvir.Log($"{connection.IPAddress} Disconnected, Large Packet"); + return; + } + + if (connection.ReceiveList.Count > Config.MaxPacket) + { + connection.TryDisconnect(); + IpManager.Timeout(connection, Config.PacketBanTime); + SEnvir.Log($"{connection.IPAddress} Disconnected, Large amount of Packets"); + return; + } + + bytesSent += connection.TotalBytesSent; + bytesReceived += connection.TotalBytesReceived; + } + + TotalBytesSent = DBytesSent + bytesSent; + TotalBytesReceived = DBytesReceived + bytesReceived; + PreviousTotalReceived = TotalBytesReceived; + PreviousTotalSent = TotalBytesSent; + } + + internal void Broadcast(Packet packet) + { + for (int i = Connections.Count - 1; i >= 0; i--) + Connections[i].SendDisconnect(packet); + } + + internal void Broadcast(Func packet) + { + for (int i = Connections.Count - 1; i >= 0; i--) + Connections[i].SendDisconnect(packet.Invoke(Connections[i])); + } + + internal void RemoveConnection(ConnectionType connection) + { + if (!Connections.Contains(connection)) + throw new InvalidOperationException("Connection was not found in list"); //TODO: why do we want to cause an exception here? Why not just log a warning and return? + + Connections.Remove(connection); + //IPCount[connection.IPAddress]--; + DBytesSent += TotalBytesSent; + DBytesReceived += TotalBytesReceived; + } + + internal void Reset() + { + IsResetting = true; + PendingConnections?.Clear(); + + try + { + Packet p = new Library.Network.GeneralPackets.Disconnect { Reason = DisconnectReason.ServerClosing }; + for (int i = Connections.Count - 1; i >= 0; i--) + Connections[i].SendDisconnect(p); + + Thread.Sleep(2000); // wait for disconnects + + } + catch (Exception ex) + { + SEnvir.Log(ex.ToString()); + } + + IsResetting = false; + } + } +} diff --git a/ServerLibrary/Infrastructure.Network/IpAddressManager.cs b/ServerLibrary/Infrastructure.Network/IpAddressManager.cs new file mode 100644 index 00000000..c849820c --- /dev/null +++ b/ServerLibrary/Infrastructure.Network/IpAddressManager.cs @@ -0,0 +1,30 @@ +using Library.Network; +using Server.Envir; +using System; +using System.Collections.Generic; + +namespace Server.Infrastructure.Network +{ + public class IpAddressManager + { + public Dictionary IpAddressTimeOuts = []; + + public bool IsAllowing(string ipAddress) + { + return !IpAddressTimeOuts.TryGetValue(ipAddress, out DateTime timeout) || timeout <= SEnvir.Now; + } + + public void Timeout(BaseConnection connection, TimeSpan duration) + { + var bannedUntil = DateTime.Now.Add(duration); + if (!IpAddressTimeOuts.TryAdd(connection.IPAddress, bannedUntil)) + IpAddressTimeOuts[connection.IPAddress] = bannedUntil; + connection.TryDisconnect(); + } + + public void Reset() + { + IpAddressTimeOuts.Clear(); + } + } +} diff --git a/ServerLibrary/Infrastructure.Network/IpService.cs b/ServerLibrary/Infrastructure.Network/IpService.cs deleted file mode 100644 index 11d458c5..00000000 --- a/ServerLibrary/Infrastructure.Network/IpService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Server.Envir; -using System; -using System.Collections.Generic; -using System.Net; - -namespace Server.Infrastructure.Network -{ - public class IpService - { - public static Dictionary IPBlocks = new Dictionary(); //TODO: this should be supplied & encapsulated in a Service (i.e ConnectionService.CanConnect(IpAddress)) - - public bool IsBanned(SConnection connection) - { - return IsBanned(connection.IPAddress); - } - - public bool IsBanned(string ipAddress) - { - return IPBlocks.TryGetValue(ipAddress, out DateTime bannedUntil) && bannedUntil > SEnvir.Now; - } - - public void Ban(SConnection connection, TimeSpan duration) - { - var bannedUntil = DateTime.Now.Add(duration); - if (!IPBlocks.TryAdd(connection.IPAddress, bannedUntil)) - IPBlocks[connection.IPAddress] = bannedUntil; - connection.TryDisconnect(); - } - - public void Ban(SConnection connection) - { - Ban(connection, TimeSpan.MaxValue); - } - - public void Reset() - { - IPBlocks.Clear(); - } - } -} diff --git a/ServerLibrary/Infrastructure.Network/SConnection.cs b/ServerLibrary/Infrastructure.Network/SConnection.cs index fd4a9bb2..a67156a1 100644 --- a/ServerLibrary/Infrastructure.Network/SConnection.cs +++ b/ServerLibrary/Infrastructure.Network/SConnection.cs @@ -29,7 +29,6 @@ public sealed class SConnection : BaseConnection public GameStage Stage { get; set; } public AccountInfo Account { get; set; } public PlayerObject Player { get; set; } - public string IPAddress { get; } public int SessionID { get; } public SConnection Observed; @@ -42,9 +41,12 @@ public sealed class SConnection : BaseConnection // new S.SystemMessage { id = 1 }; public StringMessages Language; - public SConnection(TcpClient client) : base(client) + public Action DisconnectCallback; + + public SConnection(TcpClient client, Action disconnectCallback) : base(client) { - IPAddress = client.Client.RemoteEndPoint.ToString().Split(':')[0]; + DisconnectCallback = disconnectCallback; + SessionID = ++SessionCount; Language = (StringMessages)ConfigReader.ConfigObjects[typeof(EnglishMessages)]; @@ -65,20 +67,21 @@ public SConnection(TcpClient client) : base(client) Enqueue(new G.Connected()); } + //TODO: whats the difference between Disconnect and SendDisconnect? Can we consolodate the two? public override void Disconnect() { if (!Connected) return; base.Disconnect(); CleanUp(); - TcpServer.Disconnect(this); + DisconnectCallback.Invoke(this); } public override void SendDisconnect(Packet p) { base.SendDisconnect(p); - CleanUp(); } + public override void TryDisconnect() { if (Stage == GameStage.Game) diff --git a/ServerLibrary/Infrastructure.Network/SConnectionFactory.cs b/ServerLibrary/Infrastructure.Network/SConnectionFactory.cs new file mode 100644 index 00000000..4bde5452 --- /dev/null +++ b/ServerLibrary/Infrastructure.Network/SConnectionFactory.cs @@ -0,0 +1,15 @@ +using System; +using System.Net.Sockets; + +namespace Server.Infrastructure.Network +{ + internal class SConnectionFactory(Func> disconnectCallback) : IConnectionFactory + { + private readonly Func> DisconnectCallback = disconnectCallback; + + public SConnection Create(TcpClient tcpClient) + { + return new SConnection(tcpClient, DisconnectCallback.Invoke()); + } + } +} diff --git a/ServerLibrary/Infrastructure.Network/SConnectionManager.cs b/ServerLibrary/Infrastructure.Network/SConnectionManager.cs new file mode 100644 index 00000000..e362c2ff --- /dev/null +++ b/ServerLibrary/Infrastructure.Network/SConnectionManager.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq; + +namespace Server.Infrastructure.Network +{ + public class SConnectionManager(IConnectionFactory connectionFactory, IpAddressManager IpManager) + : ConnectionManager(connectionFactory, IpManager) + { + public int Players => Connections?.Count(c => c.Stage == GameStage.Game) ?? 0; + public int Observers => Connections?.Count(c => c.Stage == GameStage.Observer) ?? 0; + } +} diff --git a/ServerLibrary/Infrastructure.Network/TcpServer.cs b/ServerLibrary/Infrastructure.Network/TcpServer.cs index c92d07b9..9eccab8e 100644 --- a/ServerLibrary/Infrastructure.Network/TcpServer.cs +++ b/ServerLibrary/Infrastructure.Network/TcpServer.cs @@ -1,118 +1,47 @@ -using Library; -using Library.Network; -using Server.DBModels; +using Library.Network; using Server.Envir; using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; -using G = Library.Network.GeneralPackets; namespace Server.Infrastructure.Network { - public class TcpServer + public class TcpServer(ConnectionManager ConnectionManager) where ConnectionType : BaseConnection { - public static bool NetworkStarted { get; set; } + public bool Started { get; private set; } + public long DownloadSpeed => ConnectionManager.TotalBytesReceived - ConnectionManager.PreviousTotalReceived; + public long UploadSpeed => ConnectionManager.TotalBytesSent - ConnectionManager.PreviousTotalSent; - public readonly IpService IpService; - //public static Dictionary IPCount = new Dictionary(); //TODO: this isn't used but i imagine its intended to limit connections from single IP - - public static List Connections = new List(); - public static ConcurrentQueue NewConnections; - - private static TcpListener _listener, _userCountListener; - - public static long DBytesSent, DBytesReceived; - - public static long TotalBytesSent, TotalBytesReceived; - public static long PreviousTotalReceived, PreviousTotalSent; - - public static long DownloadSpeed, UploadSpeed; - - public TcpServer(IpService ipBanService) - { - IpService = ipBanService; - } + private TcpListener _listener, + _userCountListener; //TODO: move this out - inject ConnectionHandler, inject port and create separate tcpServer for handling UC public void StartNetwork(bool log = true) { try { - NewConnections = new ConcurrentQueue(); - _listener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.Port); _listener.Start(); - _listener.BeginAcceptTcpClient(Connection, null); + _listener.BeginAcceptTcpClient(TcpConnection, null); _userCountListener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.UserCountPort); _userCountListener.Start(); _userCountListener.BeginAcceptTcpClient(CountConnection, null); - NetworkStarted = true; + Started = true; if (log) SEnvir.Log($"Network Started. Listen: {Config.IPAddress}:{Config.Port}"); } catch (Exception ex) { - NetworkStarted = false; + Started = false; SEnvir.Log(ex.ToString()); } } public void Process() { - SConnection connection; - while (!NewConnections.IsEmpty) - { - if (!NewConnections.TryDequeue(out connection)) break; - - //IPCount.TryGetValue(connection.IPAddress, out var ipCount); - //IPCount[connection.IPAddress] = ipCount + 1; - - Connections.Add(connection); - } - - long bytesSent = 0; - long bytesReceived = 0; - - for (int i = Connections.Count - 1; i >= 0; i--) - { - if (i >= Connections.Count) break; - - connection = Connections[i]; - connection.Process(); - - if (connection.TotalPacketsProcessed == 0 && connection.TotalBytesReceived > 1024) - { - connection.TryDisconnect(); - IpService.Ban(connection, Config.PacketBanTime); - SEnvir.Log($"{connection.IPAddress} Disconnected, Large Packet"); - return; - } - - if (connection.ReceiveList.Count > Config.MaxPacket) - { - connection.TryDisconnect(); - IpService.Ban(connection, Config.PacketBanTime); - SEnvir.Log($"{connection.IPAddress} Disconnected, Large amount of Packets"); - return; - } - - bytesSent += connection.TotalBytesSent; - bytesReceived += connection.TotalBytesReceived; - } - - TotalBytesSent = DBytesSent + bytesSent; - TotalBytesReceived = DBytesReceived + bytesReceived; - - DownloadSpeed = TotalBytesReceived - PreviousTotalReceived; - UploadSpeed = TotalBytesSent - PreviousTotalSent; - - PreviousTotalReceived = TotalBytesReceived; - PreviousTotalSent = TotalBytesSent; + ConnectionManager.Process(); } public void StopNetwork(bool log = true) @@ -123,137 +52,43 @@ public void StopNetwork(bool log = true) _listener = null; _userCountListener = null; - NetworkStarted = false; + Started = false; expiredListener?.Stop(); expiredUserListener?.Stop(); - - NewConnections = null; - - try - { - Packet p = new G.Disconnect { Reason = DisconnectReason.ServerClosing }; - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(p); - - Thread.Sleep(2000); - } - catch (Exception ex) - { - SEnvir.Log(ex.ToString()); - } + ConnectionManager?.Reset(); if (log) SEnvir.Log("Network Stopped."); } - //TODO: work out what SendDisconnect is doing differently to a Enqueue and see if we can consolodate. public void Broadcast(Packet packet) { - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(packet); + ConnectionManager.Broadcast(packet); } - #region GameEngine Logic Stuff - Move Out - //TODO: pull out the chat logic from here - its not a network component - internal static void BroadcastOnlineMessage() - { - foreach (SConnection conn in Connections) - { - conn.ReceiveChat(string.Format(conn.Language.OnlineCount, Connections.Count(x => x.Stage == GameStage.Game), Connections.Count(x => x.Stage == GameStage.Observer)), MessageType.Hint); - - switch (conn.Stage) - { - case GameStage.Game: - if (conn.Player.Character.Observable) - conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observers.Count), MessageType.Hint); - break; - case GameStage.Observer: - conn.ReceiveChat(string.Format(conn.Language.ObserverCount, conn.Observed.Observers.Count), MessageType.Hint); - break; - } - } - } - - internal static void BroadcastSystemMessage(Func messageSupplier) - { - foreach (SConnection con in Connections) - { - switch (con.Stage) - { - case GameStage.Game: - case GameStage.Observer: - con.ReceiveChat(messageSupplier.Invoke(con), MessageType.System); - break; - default: - continue; - } - } - } - - internal static void BroadcastMessage(string text, List linkedItems, MessageType messageType, Predicate shouldReceive) - { - foreach (SConnection con in Connections) - { - switch (con.Stage) - { - case GameStage.Game: - case GameStage.Observer: - if (!shouldReceive.Invoke(con)) continue; - - con.ReceiveChat(text, messageType, linkedItems); - break; - default: continue; - } - } - } - #endregion - - internal static void Disconnect(SConnection connection) - { - if (!Connections.Contains(connection)) - throw new InvalidOperationException("Connection was not found in list"); //TODO: do we want to cause an exception here? Why not just log a warning and return? - - Connections.Remove(connection); - //IPCount[connection.IPAddress]--; - DBytesSent += TotalBytesSent; - DBytesReceived += TotalBytesReceived; - } - - #region Connection Stuff - Move out - private void Connection(IAsyncResult result) + private void TcpConnection(IAsyncResult result) { try { if (_listener == null || !_listener.Server.IsBound) return; - - TcpClient client = _listener.EndAcceptTcpClient(result); - - string ipAddress = client.Client.RemoteEndPoint.ToString().Split(':')[0]; - if (IpService.IsBanned(ipAddress)) - { - SConnection Connection = new SConnection(client); - - if (Connection.Connected) - NewConnections?.Enqueue(Connection); - } - } - catch (SocketException) - { - + ConnectionManager.Add(_listener.EndAcceptTcpClient(result)); } + catch (SocketException) { } catch (Exception ex) { SEnvir.Log(ex.ToString()); } finally { - while (NewConnections?.Count >= 15) + while (ConnectionManager.AcceptingConnections) Thread.Sleep(1); if (_listener != null && _listener.Server.IsBound) - _listener.BeginAcceptTcpClient(Connection, null); + _listener.BeginAcceptTcpClient(TcpConnection, null); } } + + #region Count Connection Stuff - Separate into a different TcpServer private void CountConnection(IAsyncResult result) { try @@ -261,9 +96,7 @@ private void CountConnection(IAsyncResult result) if (_userCountListener == null || !_userCountListener.Server.IsBound) return; TcpClient client = _userCountListener.EndAcceptTcpClient(result); - - byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", Connections.Count)); - + byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", ConnectionManager.Connections.Count)); client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, CountConnectionEnd, client); } catch { } @@ -277,12 +110,10 @@ private void CountConnectionEnd(IAsyncResult result) { try { - TcpClient client = result.AsyncState as TcpClient; + var client = result.AsyncState as TcpClient; if (client == null) return; - client.Client.EndSend(result); - client.Client.Dispose(); } catch { } diff --git a/ServerLibrary/Models/ConquestWar.cs b/ServerLibrary/Models/ConquestWar.cs index 9ab38c5a..d3f4c3b0 100644 --- a/ServerLibrary/Models/ConquestWar.cs +++ b/ServerLibrary/Models/ConquestWar.cs @@ -24,11 +24,10 @@ public sealed class ConquestWar public Dictionary Stats = new Dictionary(); + public void StartWar() { - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestStarted, Castle.Name)); - - + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestStarted, Castle.Name)); Map = SEnvir.GetMap(Castle.Map); for (int i = Map.NPCs.Count - 1; i >= 0; i--) @@ -62,7 +61,7 @@ public void Process() public void EndWar() { - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestFinished, Castle.Name)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestFinished, Castle.Name)); Ended = true; @@ -87,7 +86,7 @@ public void EndWar() if (ownerGuild != null) { - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestOwner, ownerGuild.GuildName, Castle.Name)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestOwner, ownerGuild.GuildName, Castle.Name)); UserConquest conquest = SEnvir.UserConquestList.Binding.FirstOrDefault(x => x.Castle == Castle && x.Castle == ownerGuild?.Castle); diff --git a/ServerLibrary/Models/Map.cs b/ServerLibrary/Models/Map.cs index e7f4d522..ff100554 100644 --- a/ServerLibrary/Models/Map.cs +++ b/ServerLibrary/Models/Map.cs @@ -443,11 +443,11 @@ public void DoSpawn(bool eventSpawn) { if (Info.Delay >= 1000000) { - TcpServer.BroadcastMessage($"{mob.MonsterInfo.MonsterName} has appeared.", null, MessageType.System, c => true); + SEnvir.BroadcastService.BroadcastSystemMessage(c => $"{mob.MonsterInfo.MonsterName} has appeared."); } else { - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.BossSpawn, CurrentMap.Info.Description)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.BossSpawn, CurrentMap.Info.Description)); } } diff --git a/ServerLibrary/Models/Monsters/CastleFlag.cs b/ServerLibrary/Models/Monsters/CastleFlag.cs index 5f612871..e1c15833 100644 --- a/ServerLibrary/Models/Monsters/CastleFlag.cs +++ b/ServerLibrary/Models/Monsters/CastleFlag.cs @@ -72,7 +72,7 @@ public override void Process() if (Target == null && Contester != null) { - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name)); Contester = null; ContesterTime = DateTime.MaxValue; } @@ -126,8 +126,7 @@ protected override void Attack() //Start 30 seconds timer ContesterTime = SEnvir.Now.Add(ContesterDelay); - - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, _takeDuration)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, _takeDuration)); return; } else @@ -151,7 +150,7 @@ protected override void Attack() { //Another guild near flag - reset contest time ContesterTime = SEnvir.Now.Add(ContesterDelay); - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestPreventingFlag, player.Character.Account.GuildMember.Guild.GuildName, Contester.GuildName, War.Castle.Name)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestPreventingFlag, player.Character.Account.GuildMember.Guild.GuildName, Contester.GuildName, War.Castle.Name)); return; } @@ -163,7 +162,7 @@ protected override void Attack() if (!contestGuildNear) { - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestNotTakingFlag, Contester.GuildName, War.Castle.Name)); Contester = null; ContesterTime = DateTime.MaxValue; @@ -173,7 +172,7 @@ protected override void Attack() else { var difference = (ContesterTime - SEnvir.Now).Seconds; - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, difference)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestTakingFlag, Contester.GuildName, War.Castle.Name, difference)); } } @@ -188,8 +187,7 @@ protected override void Attack() //Update new guild with castle Contester.Castle = War.Castle; - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, Contester.GuildName, War.Castle.Name)); - + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, Contester.GuildName, War.Castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = War.Castle.Index, Owner = Contester.GuildName }); Contester = null; diff --git a/ServerLibrary/Models/Monsters/CastleLord.cs b/ServerLibrary/Models/Monsters/CastleLord.cs index e35551e7..68c5e544 100644 --- a/ServerLibrary/Models/Monsters/CastleLord.cs +++ b/ServerLibrary/Models/Monsters/CastleLord.cs @@ -281,7 +281,8 @@ public override void Die() EXPOwner.Character.Account.GuildMember.Guild.Castle = War.Castle; - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, EXPOwner.Character.Account.GuildMember.Guild.GuildName, War.Castle.Name)); + + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.ConquestCapture, EXPOwner.Character.Account.GuildMember.Guild.GuildName, War.Castle.Name)); SEnvir.Broadcast(new S.GuildCastleInfo { Index = War.Castle.Index, Owner = EXPOwner.Character.Account.GuildMember.Guild.GuildName }); War.CastleTarget = null; diff --git a/ServerLibrary/Models/Monsters/JinamStoneGate.cs b/ServerLibrary/Models/Monsters/JinamStoneGate.cs index 6c11c4a9..f09a42ba 100644 --- a/ServerLibrary/Models/Monsters/JinamStoneGate.cs +++ b/ServerLibrary/Models/Monsters/JinamStoneGate.cs @@ -17,7 +17,6 @@ public class JinamStoneGate :MonsterObject public DateTime DespawnTime; - public JinamStoneGate() { Direction = MirDirection.Up; @@ -32,12 +31,9 @@ protected override void OnSpawned() { base.OnSpawned(); - DespawnTime = SEnvir.Now.AddMinutes(20); - - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.LairGateOpen, CurrentMap.Info.Description, CurrentLocation)); - - } + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.LairGateOpen, CurrentMap.Info.Description, CurrentLocation)); + } public override void Process() { @@ -48,7 +44,7 @@ public override void Process() if (SpawnInfo != null) SpawnInfo.AliveCount--; - TcpServer.BroadcastSystemMessage(c => c.Language.LairGateClosed); + SEnvir.BroadcastService.BroadcastSystemMessage(c => c.Language.LairGateClosed); SpawnInfo = null; Despawn(); diff --git a/ServerLibrary/Models/Monsters/NetherworldGate.cs b/ServerLibrary/Models/Monsters/NetherworldGate.cs index d43ab944..3a2bb9cc 100644 --- a/ServerLibrary/Models/Monsters/NetherworldGate.cs +++ b/ServerLibrary/Models/Monsters/NetherworldGate.cs @@ -12,7 +12,6 @@ public class NetherworldGate : MonsterObject public override bool CanAttack => false; public DateTime DespawnTime; - public NetherworldGate() { @@ -29,7 +28,7 @@ protected override void OnSpawned() base.OnSpawned(); DespawnTime = SEnvir.Now.AddMinutes(20); - TcpServer.BroadcastSystemMessage(c => string.Format(c.Language.NetherGateOpen, CurrentMap.Info.Description, CurrentLocation)); + SEnvir.BroadcastService.BroadcastSystemMessage(c => string.Format(c.Language.NetherGateOpen, CurrentMap.Info.Description, CurrentLocation)); } public override void Process() @@ -41,7 +40,7 @@ public override void Process() if (SpawnInfo != null) SpawnInfo.AliveCount--; - TcpServer.BroadcastSystemMessage(c => c.Language.NetherGateClosed); + SEnvir.BroadcastService.BroadcastSystemMessage(c => c.Language.NetherGateClosed); SpawnInfo = null; Despawn(); diff --git a/ServerLibrary/Models/PlayerObject.cs b/ServerLibrary/Models/PlayerObject.cs index 9ad20ddf..7f85ff35 100644 --- a/ServerLibrary/Models/PlayerObject.cs +++ b/ServerLibrary/Models/PlayerObject.cs @@ -1570,7 +1570,7 @@ public void Chat(string text) } text = string.Format("(!@){0}: {1}", Name, text.Remove(0, 2)); - TcpServer.BroadcastMessage(text, linkedItems, MessageType.Global, c => !SEnvir.IsBlocking(Character.Account, c.Account)); + SEnvir.BroadcastService.BroadcastMessage(text, linkedItems, MessageType.Global, c => !SEnvir.IsBlocking(Character.Account, c.Account)); } else if (text.StartsWith("!")) @@ -1613,7 +1613,7 @@ public void Chat(string text) if (!Character.Account.TempAdmin) return; text = string.Format("{0}: {1}", Name, text.Remove(0, 2)); - TcpServer.BroadcastMessage(text, linkedItems, MessageType.Announcement, c => true); + SEnvir.BroadcastService.BroadcastMessage(text, linkedItems, MessageType.Announcement, c => true); } else if (text.StartsWith("@")) { @@ -1738,14 +1738,14 @@ public void ObserverChat(SConnection con, string text) } text = string.Format("(!@){0}: {1}", con.Account.LastCharacter.CharacterName, text.Remove(0, 2)); - TcpServer.BroadcastMessage(text, null, MessageType.Global, c => SEnvir.IsBlocking(con.Account, c.Account)); + SEnvir.BroadcastService.BroadcastMessage(text, null, MessageType.Global, c => SEnvir.IsBlocking(con.Account, c.Account)); } else if (text.StartsWith("@!")) { if (!con.Account.LastCharacter.Account.TempAdmin) return; text = string.Format("{0}: {1}", con.Account.LastCharacter.CharacterName, text.Remove(0, 2)); - TcpServer.BroadcastMessage(text, null, MessageType.Announcement, c => true); + SEnvir.BroadcastService.BroadcastMessage(text, null, MessageType.Announcement, c => true); } else { @@ -5988,7 +5988,7 @@ public void ItemUse(CellLinkInfo link) string text = $"A [{item.Info.ItemName}] has been used in {CurrentMap.Info.Description}"; - TcpServer.BroadcastMessage(text, null, MessageType.System, c => true); + SEnvir.BroadcastService.BroadcastMessage(text, null, MessageType.System, c => true); } } } From 40ae07d14c680caf53aac3f6b218328d8eac42db Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 4 Aug 2025 22:12:55 +0100 Subject: [PATCH 4/6] Finalising TcpServer logic by making it single purpose, separating user listner handler and user count listener handler. Setup Network.Smtp and Network.Http to also make way for future improvements --- Server/SMain.cs | 63 ++++----- Server/Views/SyncForm.cs | 15 +-- ServerLibrary/DBModels/AccountInfo.cs | 4 +- .../Commands/Command/Admin/GiveGameGold.cs | 2 +- .../Commands/ErrorHandlingCommandHandler.cs | 4 +- ServerLibrary/Envir/SEnvir.cs | 95 ++++++++------ .../SConnectionFactory.cs | 15 --- .../SConnectionManager.cs | 12 -- .../Infrastructure.Network/TcpServer.cs | 123 ------------------ .../Network/Http/HttpWebServer.cs} | 16 +-- .../Network/Smtp}/EmailService.cs | 14 +- .../ActiveUserCountListenerHandler.cs | 39 ++++++ .../Tcp/ListenerHandler/IListenerHandler.cs | 17 +++ .../UserConnectionListenerHandler.cs | 28 ++++ .../Infrastructure/Network/Tcp/TcpServer.cs | 60 +++++++++ .../Connection/AbstractConnectionService.cs} | 35 ++--- .../Service/Connection/IConnectionFactory.cs} | 2 +- .../Service/Connection/UserConnection.cs} | 22 ++-- .../Connection/UserConnectionFactory.cs | 15 +++ .../Connection/UserConnectionService.cs | 12 ++ .../Service/IpAddressService.cs} | 4 +- .../Service/UserBroadcastService.cs} | 27 ++-- ServerLibrary/Models/PlayerObject.cs | 34 ++--- 23 files changed, 345 insertions(+), 313 deletions(-) delete mode 100644 ServerLibrary/Infrastructure.Network/SConnectionFactory.cs delete mode 100644 ServerLibrary/Infrastructure.Network/SConnectionManager.cs delete mode 100644 ServerLibrary/Infrastructure.Network/TcpServer.cs rename ServerLibrary/{Envir/WebServer.cs => Infrastructure/Network/Http/HttpWebServer.cs} (98%) rename ServerLibrary/{Envir => Infrastructure/Network/Smtp}/EmailService.cs (94%) create mode 100644 ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs create mode 100644 ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs create mode 100644 ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs create mode 100644 ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs rename ServerLibrary/{Infrastructure.Network/ConnectionManager.cs => Infrastructure/Service/Connection/AbstractConnectionService.cs} (75%) rename ServerLibrary/{Infrastructure.Network/ConnectionFactory.cs => Infrastructure/Service/Connection/IConnectionFactory.cs} (81%) rename ServerLibrary/{Infrastructure.Network/SConnection.cs => Infrastructure/Service/Connection/UserConnection.cs} (97%) create mode 100644 ServerLibrary/Infrastructure/Service/Connection/UserConnectionFactory.cs create mode 100644 ServerLibrary/Infrastructure/Service/Connection/UserConnectionService.cs rename ServerLibrary/{Infrastructure.Network/IpAddressManager.cs => Infrastructure/Service/IpAddressService.cs} (91%) rename ServerLibrary/{Envir/BroadcastService.cs => Infrastructure/Service/UserBroadcastService.cs} (61%) diff --git a/Server/SMain.cs b/Server/SMain.cs index 5d52d5a4..2c950b23 100644 --- a/Server/SMain.cs +++ b/Server/SMain.cs @@ -8,6 +8,7 @@ using PluginCore; using Server.DBModels; using Server.Envir; +using Server.Infrastructure.Network.Smtp; using Server.Views; using System; using System.Collections.Generic; @@ -188,7 +189,7 @@ private void UpdateInterface() StartServerButton.Enabled = SEnvir.EnvirThread == null; StopServerButton.Enabled = SEnvir.Started; - ConnectionLabel.Caption = string.Format(@"Connections: {0:#,##0}", SEnvir.SConnectionManager.Connections.Count); + ConnectionLabel.Caption = string.Format(@"Connections: {0:#,##0}", SEnvir.UserConnectionService.ActiveConnections.Count); ObjectLabel.Caption = string.Format(@"Objects: {0} of {1:#,##0}", SEnvir.ActiveObjects.Count, SEnvir.Objects.Count); ProcessLabel.Caption = string.Format(@"Process Count: {0:#,##0}", SEnvir.ProcessObjectCount); LoopLabel.Caption = string.Format(@"Loop Count: {0:#,##0}", SEnvir.LoopCount); @@ -201,42 +202,42 @@ private void UpdateInterface() const decimal MB = KB * 1024; const decimal GB = MB * 1024; - if (SEnvir.SConnectionManager.TotalBytesReceived > GB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}GB", SEnvir.SConnectionManager.TotalBytesReceived / GB); - else if (SEnvir.SConnectionManager.TotalBytesReceived > MB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}MB", SEnvir.SConnectionManager.TotalBytesReceived / MB); - else if (SEnvir.SConnectionManager.TotalBytesReceived > KB) - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}KB", SEnvir.SConnectionManager.TotalBytesReceived / KB); + if (SEnvir.UserConnectionService.TotalBytesReceived > GB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}GB", SEnvir.UserConnectionService.TotalBytesReceived / GB); + else if (SEnvir.UserConnectionService.TotalBytesReceived > MB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0.0}MB", SEnvir.UserConnectionService.TotalBytesReceived / MB); + else if (SEnvir.UserConnectionService.TotalBytesReceived > KB) + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}KB", SEnvir.UserConnectionService.TotalBytesReceived / KB); else - TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}B", SEnvir.SConnectionManager.TotalBytesReceived); - - if (SEnvir.SConnectionManager.TotalBytesSent > GB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}GB", SEnvir.SConnectionManager.TotalBytesSent / GB); - else if (SEnvir.SConnectionManager.TotalBytesSent > MB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}MB", SEnvir.SConnectionManager.TotalBytesSent / MB); - else if (SEnvir.SConnectionManager.TotalBytesSent > KB) - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}KB", SEnvir.SConnectionManager.TotalBytesSent / KB); + TotalDownloadLabel.Caption = string.Format(@"Downloaded: {0:#,##0}B", SEnvir.UserConnectionService.TotalBytesReceived); + + if (SEnvir.UserConnectionService.TotalBytesSent > GB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}GB", SEnvir.UserConnectionService.TotalBytesSent / GB); + else if (SEnvir.UserConnectionService.TotalBytesSent > MB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0.0}MB", SEnvir.UserConnectionService.TotalBytesSent / MB); + else if (SEnvir.UserConnectionService.TotalBytesSent > KB) + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}KB", SEnvir.UserConnectionService.TotalBytesSent / KB); else - TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}B", SEnvir.SConnectionManager.TotalBytesSent); + TotalUploadLabel.Caption = string.Format(@"Uploaded: {0:#,##0}B", SEnvir.UserConnectionService.TotalBytesSent); - if (SEnvir.TcpServer.DownloadSpeed > GB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}GBps", SEnvir.TcpServer.DownloadSpeed / GB); - else if (SEnvir.TcpServer.DownloadSpeed > MB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}MBps", SEnvir.TcpServer.DownloadSpeed / MB); - else if (SEnvir.TcpServer.DownloadSpeed > KB) - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}KBps", SEnvir.TcpServer.DownloadSpeed / KB); + if (SEnvir.UserConnectionService.DownloadSpeed > GB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}GBps", SEnvir.UserConnectionService.DownloadSpeed / GB); + else if (SEnvir.UserConnectionService.DownloadSpeed > MB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0.0}MBps", SEnvir.UserConnectionService.DownloadSpeed / MB); + else if (SEnvir.UserConnectionService.DownloadSpeed > KB) + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}KBps", SEnvir.UserConnectionService.DownloadSpeed / KB); else - DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}Bps", SEnvir.TcpServer.DownloadSpeed); - - if (SEnvir.TcpServer.UploadSpeed > GB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}GBps", SEnvir.TcpServer.UploadSpeed / GB); - else if (SEnvir.TcpServer.UploadSpeed > MB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}MBps", SEnvir.TcpServer.UploadSpeed / MB); - else if (SEnvir.TcpServer.UploadSpeed > KB) - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}KBps", SEnvir.TcpServer.UploadSpeed / KB); + DownloadSpeedLabel.Caption = string.Format(@"D/L Speed: {0:#,##0}Bps", SEnvir.UserConnectionService.DownloadSpeed); + + if (SEnvir.UserConnectionService.UploadSpeed > GB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}GBps", SEnvir.UserConnectionService.UploadSpeed / GB); + else if (SEnvir.UserConnectionService.UploadSpeed > MB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0.0}MBps", SEnvir.UserConnectionService.UploadSpeed / MB); + else if (SEnvir.UserConnectionService.UploadSpeed > KB) + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}KBps", SEnvir.UserConnectionService.UploadSpeed / KB); else - UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}Bps", SEnvir.TcpServer.UploadSpeed); + UploadSpeedLabel.Caption = string.Format(@"U/L Speed: {0:#,##0}Bps", SEnvir.UserConnectionService.UploadSpeed); } private void StartServerButton_ItemClick(object sender, DevExpress.XtraBars.ItemClickEventArgs e) diff --git a/Server/Views/SyncForm.cs b/Server/Views/SyncForm.cs index 5ed303e3..0d7ab03e 100644 --- a/Server/Views/SyncForm.cs +++ b/Server/Views/SyncForm.cs @@ -1,17 +1,8 @@ -using MirDB; -using Server.Envir; +using Server.Envir; +using Server.Infrastructure.Network.Http; using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; using System.IO; -using System.IO.Compression; -using System.Linq; -using System.Net; using System.Net.Http; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; namespace Server.Views @@ -33,7 +24,7 @@ private void btnSync_Click(object sender, EventArgs e) string key = Uri.EscapeDataString(txtKey.Text); - var url = $"{txtRemoteIP.Text}?Type={WebServer.SystemDBSyncCommand}&Key={key}"; + var url = $"{txtRemoteIP.Text}?Type={HttpWebServer.SystemDBSyncCommand}&Key={key}"; try { HttpResponseMessage response = client.PostAsync(url, content).Result; diff --git a/ServerLibrary/DBModels/AccountInfo.cs b/ServerLibrary/DBModels/AccountInfo.cs index c5d6f6f6..7d600e8c 100644 --- a/ServerLibrary/DBModels/AccountInfo.cs +++ b/ServerLibrary/DBModels/AccountInfo.cs @@ -2,7 +2,7 @@ using Library.SystemModels; using MirDB; using Server.Envir; -using Server.Infrastructure.Network; +using Server.Infrastructure.Service.Connection; using System; using System.Collections.Generic; using System.Linq; @@ -618,7 +618,7 @@ public CharacterInfo LastCharacter public int WrongPasswordCount; - public SConnection Connection; + public UserConnection Connection; public string Key; protected override void OnCreated() diff --git a/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs b/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs index 124f2257..53c50043 100644 --- a/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs +++ b/ServerLibrary/Envir/Commands/Command/Admin/GiveGameGold.cs @@ -3,7 +3,7 @@ using Server.Envir.Commands.Command; using Server.Envir.Commands.Command.Admin; using Server.Envir.Commands.Exceptions; -using Server.Infrastructure.Network; +using Server.Infrastructure.Service.Connection; using Server.Models; namespace Server.Envir.Commands.Admin diff --git a/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs b/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs index eb92b6ae..b1b4dde4 100644 --- a/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs +++ b/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs @@ -1,7 +1,7 @@ using Library; using Server.Envir.Commands.Exceptions; using Server.Envir.Commands.Handler; -using Server.Infrastructure.Network; +using Server.Infrastructure.Service.Connection; using Server.Models; using System; using System.Collections.Generic; @@ -42,7 +42,7 @@ public void Handle(PlayerObject player, string[] commandParts) player.Connection.ReceiveChat(exception.Message, MessageType.System); if (!exception.userOnly) { - foreach (SConnection connection in player.Connection.Observers) + foreach (UserConnection connection in player.Connection.Observers) connection.ReceiveChat(exception.Message, MessageType.System); } } diff --git a/ServerLibrary/Envir/SEnvir.cs b/ServerLibrary/Envir/SEnvir.cs index 58002801..17bc3888 100644 --- a/ServerLibrary/Envir/SEnvir.cs +++ b/ServerLibrary/Envir/SEnvir.cs @@ -7,7 +7,12 @@ using Server.Envir.Commands.Handler; using Server.Envir.Events; using Server.Envir.Events.Triggers; -using Server.Infrastructure.Network; +using Server.Infrastructure.Network.Http; +using Server.Infrastructure.Network.Smtp; +using Server.Infrastructure.Network.Tcp; +using Server.Infrastructure.Network.Tcp.ListenerHandler; +using Server.Infrastructure.Service; +using Server.Infrastructure.Service.Connection; using Server.Models; using System; using System.Collections.Concurrent; @@ -68,22 +73,32 @@ public static void LogChat(string log) #endregion - public static readonly IpAddressManager IpManager = new IpAddressManager(); - public static readonly SConnectionManager SConnectionManager = new SConnectionManager( - new SConnectionFactory(() => SConnectionManager.RemoveConnection), + public static readonly IpAddressService IpManager = new IpAddressService(); + public static readonly UserConnectionService UserConnectionService = new UserConnectionService( + new UserConnectionFactory(() => UserConnectionService.RemoveConnection), IpManager ); - public static readonly TcpServer TcpServer = new TcpServer(SConnectionManager); - public static BroadcastService BroadcastService = new BroadcastService(SConnectionManager); - public static bool Started + public static readonly TcpServer UserTcpServer = new TcpServer( + new UserConnectionListenerHandler(UserConnectionService), + Config.IPAddress, + Config.Port + ); + public static readonly TcpServer ActiveUserCountTcpServer = new TcpServer( + new ActiveUserCountListenerHandler(() => UserConnectionService.ActiveConnections.Count), + Config.IPAddress, + Config.UserCountPort + ); + public static readonly UserBroadcastService BroadcastService = new UserBroadcastService(UserConnectionService); + + public static bool Started //TODO: not sure what purpose of this was really.. it was 100% controlled by UserTcpServer { - get => TcpServer.Started; + get => UserTcpServer.Started; set { - if (TcpServer.Started == value) return; - if (value) TcpServer.StartNetwork(); - else TcpServer.StopNetwork(); + if (UserTcpServer.Started == value) return; + if (value) UserTcpServer.Start(); + else UserTcpServer.Stop(); } } public static bool Saving { get; private set; } @@ -906,8 +921,10 @@ public static void EnvirLoop() DateTime DBTime = Now + Config.DBSaveDelay; StartEnvir(); - TcpServer.StartNetwork(); - WebServer.StartWebServer(); + UserTcpServer.Start(); + ActiveUserCountTcpServer.Start(); + SEnvir.Log($"Network Started. Listen: {Config.IPAddress}:{Config.Port}"); + HttpWebServer.StartWebServer(); int count = 0, loopCount = 0; DateTime nextCount = Now.AddSeconds(1), UserCountTime = Now.AddMinutes(5), EventTimerTime = Now.AddMinutes(1), saveTime; @@ -927,7 +944,7 @@ public static void EnvirLoop() try { - TcpServer.Process(); + UserConnectionService.Process(); long delay = (Time.Now - Now).Ticks / TimeSpan.TicksPerMillisecond; if (delay > conDelay) @@ -999,7 +1016,7 @@ public static void EnvirLoop() if (Now >= UserCountTime) { UserCountTime = Now.AddMinutes(5); - BroadcastService.SendOnlineCount(); + BroadcastService.BroadcastOnlineCount(); } if (Now >= EventTimerTime) @@ -1055,7 +1072,7 @@ public static void EnvirLoop() if (Config.EnableWebServer) { - WebServer.Process(); + HttpWebServer.Process(); } if (Config.ProcessGameGold) @@ -1099,15 +1116,17 @@ public static void EnvirLoop() Log(ex.StackTrace); File.AppendAllText(@".\Errors.txt", ex.StackTrace + Environment.NewLine); - TcpServer.Broadcast(new G.Disconnect { Reason = DisconnectReason.Crashed }); + UserConnectionService.Broadcast(new G.Disconnect { Reason = DisconnectReason.Crashed }); Thread.Sleep(3000); break; } } - WebServer.StopWebServer(); - TcpServer.StopNetwork(); + HttpWebServer.StopWebServer(); + UserTcpServer.Stop(); + ActiveUserCountTcpServer.Stop(); + SEnvir.Log("Network Stopped."); while (Saving) Thread.Sleep(1); if (Session != null) @@ -1120,13 +1139,13 @@ public static void EnvirLoop() public static void ProcessGameGold() { - while (!WebServer.Messages.IsEmpty) + while (!HttpWebServer.Messages.IsEmpty) { IPNMessage message; - if (!WebServer.Messages.TryDequeue(out message) || message == null) return; + if (!HttpWebServer.Messages.TryDequeue(out message) || message == null) return; - WebServer.PaymentList.Add(message); + HttpWebServer.PaymentList.Add(message); if (!message.Verified) { @@ -1222,17 +1241,17 @@ public static void ProcessGameGold() case "Completed": break; } - if (payment.Status != WebServer.Completed) continue; + if (payment.Status != HttpWebServer.Completed) continue; //check that receiver_email is my primary paypal email if (string.Compare(payment.Receiver_EMail, Config.ReceiverEMail, StringComparison.OrdinalIgnoreCase) != 0) payment.Error = true; //check that paymentamount/current are correct - if (payment.Currency != WebServer.Currency) + if (payment.Currency != HttpWebServer.Currency) payment.Error = true; - if (WebServer.GoldTable.TryGetValue(payment.Price, out tempInt)) + if (HttpWebServer.GoldTable.TryGetValue(payment.Price, out tempInt)) payment.GameGoldAmount = tempInt; else payment.Error = true; @@ -1274,7 +1293,7 @@ private static void Save() Saving = true; Session.Save(false); - WebServer.Save(); + HttpWebServer.Save(); Thread saveThread = new Thread(CommitChanges) { IsBackground = true }; saveThread.Start(Session); @@ -1284,7 +1303,7 @@ private static void CommitChanges(object data) Session session = (Session)data; session?.Commit(); - WebServer.CommitChanges(data); + HttpWebServer.CommitChanges(data); Saving = false; } @@ -2578,7 +2597,7 @@ public static void UpgradeLootBox(UserItem item) item.AddStat(Stat.Counter2, lootBoxInfo.Contents.Count <= 15 ? 2 : 1, StatSource.Added); // Step 1 = Randomise, 2 = Selection } - public static void Login(C.Login p, SConnection con) + public static void Login(C.Login p, UserConnection con) { AccountInfo account = null; bool admin = false; @@ -2735,7 +2754,7 @@ public static void Login(C.Login p, SConnection con) Log($"[Account Logon] Admin: {admin}, Account: {account.EMailAddress}, IP Address: {account.LastIP}, Security: {p.CheckSum}"); } - public static void NewAccount(C.NewAccount p, SConnection con) + public static void NewAccount(C.NewAccount p, UserConnection con) { if (!Config.AllowNewAccount) { @@ -2855,7 +2874,7 @@ public static void NewAccount(C.NewAccount p, SConnection con) Log($"[Account Created] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } - public static void ChangePassword(C.ChangePassword p, SConnection con) + public static void ChangePassword(C.ChangePassword p, UserConnection con) { if (!Config.AllowChangePassword) { @@ -2938,7 +2957,7 @@ public static void ChangePassword(C.ChangePassword p, SConnection con) Log($"[Password Changed] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } - public static void RequestPasswordReset(C.RequestPasswordReset p, SConnection con) + public static void RequestPasswordReset(C.RequestPasswordReset p, UserConnection con) { if (!Config.AllowRequestPasswordReset) { @@ -2983,7 +3002,7 @@ public static void RequestPasswordReset(C.RequestPasswordReset p, SConnection co Log($"[Request Password] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } - public static void ResetPassword(C.ResetPassword p, SConnection con) + public static void ResetPassword(C.ResetPassword p, UserConnection con) { if (!Config.AllowManualResetPassword) { @@ -3026,7 +3045,7 @@ public static void ResetPassword(C.ResetPassword p, SConnection con) Log($"[Reset Password] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } - public static void Activation(C.Activation p, SConnection con) + public static void Activation(C.Activation p, UserConnection con) { if (!Config.AllowManualActivation) { @@ -3055,7 +3074,7 @@ public static void Activation(C.Activation p, SConnection con) Log($"[Activation] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } - public static void RequestActivationKey(C.RequestActivationKey p, SConnection con) + public static void RequestActivationKey(C.RequestActivationKey p, UserConnection con) { if (!Config.AllowRequestActivation) { @@ -3099,7 +3118,7 @@ public static void RequestActivationKey(C.RequestActivationKey p, SConnection co Log($"[Request Activation] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } - public static void NewCharacter(C.NewCharacter p, SConnection con) + public static void NewCharacter(C.NewCharacter p, UserConnection con) { if (!Config.AllowNewCharacter) { @@ -3256,7 +3275,7 @@ public static void NewCharacter(C.NewCharacter p, SConnection con) Log($"[Character Created] Character: {p.CharacterName}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } - public static void DeleteCharacter(C.DeleteCharacter p, SConnection con) + public static void DeleteCharacter(C.DeleteCharacter p, UserConnection con) { if (!Config.AllowDeleteCharacter) { @@ -3283,7 +3302,7 @@ public static void DeleteCharacter(C.DeleteCharacter p, SConnection con) con.Enqueue(new S.DeleteCharacter { Result = DeleteCharacterResult.NotFound }); } - public static void StartGame(C.StartGame p, SConnection con) + public static void StartGame(C.StartGame p, UserConnection con) { if (!Config.AllowStartGame) { @@ -3398,7 +3417,7 @@ public static PlayerObject GetPlayerByCharacter(string name) { return GetCharacter(name)?.Account.Connection?.Player; } - public static SConnection GetConnectionByCharacter(string name) + public static UserConnection GetConnectionByCharacter(string name) { return GetCharacter(name)?.Account.Connection; } diff --git a/ServerLibrary/Infrastructure.Network/SConnectionFactory.cs b/ServerLibrary/Infrastructure.Network/SConnectionFactory.cs deleted file mode 100644 index 4bde5452..00000000 --- a/ServerLibrary/Infrastructure.Network/SConnectionFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Net.Sockets; - -namespace Server.Infrastructure.Network -{ - internal class SConnectionFactory(Func> disconnectCallback) : IConnectionFactory - { - private readonly Func> DisconnectCallback = disconnectCallback; - - public SConnection Create(TcpClient tcpClient) - { - return new SConnection(tcpClient, DisconnectCallback.Invoke()); - } - } -} diff --git a/ServerLibrary/Infrastructure.Network/SConnectionManager.cs b/ServerLibrary/Infrastructure.Network/SConnectionManager.cs deleted file mode 100644 index e362c2ff..00000000 --- a/ServerLibrary/Infrastructure.Network/SConnectionManager.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Linq; - -namespace Server.Infrastructure.Network -{ - public class SConnectionManager(IConnectionFactory connectionFactory, IpAddressManager IpManager) - : ConnectionManager(connectionFactory, IpManager) - { - public int Players => Connections?.Count(c => c.Stage == GameStage.Game) ?? 0; - public int Observers => Connections?.Count(c => c.Stage == GameStage.Observer) ?? 0; - } -} diff --git a/ServerLibrary/Infrastructure.Network/TcpServer.cs b/ServerLibrary/Infrastructure.Network/TcpServer.cs deleted file mode 100644 index 9eccab8e..00000000 --- a/ServerLibrary/Infrastructure.Network/TcpServer.cs +++ /dev/null @@ -1,123 +0,0 @@ -using Library.Network; -using Server.Envir; -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Threading; - -namespace Server.Infrastructure.Network -{ - public class TcpServer(ConnectionManager ConnectionManager) where ConnectionType : BaseConnection - { - public bool Started { get; private set; } - public long DownloadSpeed => ConnectionManager.TotalBytesReceived - ConnectionManager.PreviousTotalReceived; - public long UploadSpeed => ConnectionManager.TotalBytesSent - ConnectionManager.PreviousTotalSent; - - private TcpListener _listener, - _userCountListener; //TODO: move this out - inject ConnectionHandler, inject port and create separate tcpServer for handling UC - - public void StartNetwork(bool log = true) - { - try - { - _listener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.Port); - _listener.Start(); - _listener.BeginAcceptTcpClient(TcpConnection, null); - - _userCountListener = new TcpListener(IPAddress.Parse(Config.IPAddress), Config.UserCountPort); - _userCountListener.Start(); - _userCountListener.BeginAcceptTcpClient(CountConnection, null); - - Started = true; - if (log) SEnvir.Log($"Network Started. Listen: {Config.IPAddress}:{Config.Port}"); - } - catch (Exception ex) - { - Started = false; - SEnvir.Log(ex.ToString()); - } - } - - public void Process() - { - ConnectionManager.Process(); - } - - public void StopNetwork(bool log = true) - { - TcpListener expiredListener = _listener; - TcpListener expiredUserListener = _userCountListener; - - _listener = null; - _userCountListener = null; - - Started = false; - - expiredListener?.Stop(); - expiredUserListener?.Stop(); - ConnectionManager?.Reset(); - - if (log) SEnvir.Log("Network Stopped."); - } - - public void Broadcast(Packet packet) - { - ConnectionManager.Broadcast(packet); - } - - private void TcpConnection(IAsyncResult result) - { - try - { - if (_listener == null || !_listener.Server.IsBound) return; - ConnectionManager.Add(_listener.EndAcceptTcpClient(result)); - } - catch (SocketException) { } - catch (Exception ex) - { - SEnvir.Log(ex.ToString()); - } - finally - { - while (ConnectionManager.AcceptingConnections) - Thread.Sleep(1); - - if (_listener != null && _listener.Server.IsBound) - _listener.BeginAcceptTcpClient(TcpConnection, null); - } - } - - #region Count Connection Stuff - Separate into a different TcpServer - private void CountConnection(IAsyncResult result) - { - try - { - if (_userCountListener == null || !_userCountListener.Server.IsBound) return; - - TcpClient client = _userCountListener.EndAcceptTcpClient(result); - byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", ConnectionManager.Connections.Count)); - client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, CountConnectionEnd, client); - } - catch { } - finally - { - if (_userCountListener != null && _userCountListener.Server.IsBound) - _userCountListener.BeginAcceptTcpClient(CountConnection, null); - } - } - private void CountConnectionEnd(IAsyncResult result) - { - try - { - var client = result.AsyncState as TcpClient; - - if (client == null) return; - client.Client.EndSend(result); - client.Client.Dispose(); - } - catch { } - } - #endregion - } -} diff --git a/ServerLibrary/Envir/WebServer.cs b/ServerLibrary/Infrastructure/Network/Http/HttpWebServer.cs similarity index 98% rename from ServerLibrary/Envir/WebServer.cs rename to ServerLibrary/Infrastructure/Network/Http/HttpWebServer.cs index 349b250e..9f6d716f 100644 --- a/ServerLibrary/Envir/WebServer.cs +++ b/ServerLibrary/Infrastructure/Network/Http/HttpWebServer.cs @@ -7,18 +7,18 @@ using System.Net; using System.Text; using System.Threading.Tasks; -using System.Web; -using G = Library.Network.GeneralPackets; -using S = Library.Network.ServerPackets; -using C = Library.Network.ClientPackets; using System.Net.Http; using MirDB; using System.IO.Compression; +using Server.Infrastructure.Network.Smtp; +using Server.Envir; -namespace Server.Envir +namespace Server.Infrastructure.Network.Http { - public static class WebServer + public static class HttpWebServer { + //TODO: separate the HttpWeb components from all the other stuff like GG processing + public static ConcurrentQueue WebCommandQueue; public static bool WebServerStarted { get; set; } @@ -49,7 +49,7 @@ public static class WebServer public static ConcurrentQueue Messages = new ConcurrentQueue(); public static List PaymentList = new List(), HandledPayments = new List(); - static WebServer() + static HttpWebServer() { Messages = new ConcurrentQueue(); @@ -90,8 +90,6 @@ public static void StartWebServer(bool log = true) IPNListener.Start(); IPNListener.BeginGetContext(IPNConnection, null); - - WebServerStarted = true; if (log) SEnvir.Log("Web Server Started."); diff --git a/ServerLibrary/Envir/EmailService.cs b/ServerLibrary/Infrastructure/Network/Smtp/EmailService.cs similarity index 94% rename from ServerLibrary/Envir/EmailService.cs rename to ServerLibrary/Infrastructure/Network/Smtp/EmailService.cs index 5804f6d4..253ad450 100644 --- a/ServerLibrary/Envir/EmailService.cs +++ b/ServerLibrary/Infrastructure/Network/Smtp/EmailService.cs @@ -1,16 +1,18 @@ using Library; using Server.DBModels; +using Server.Envir; +using Server.Infrastructure.Network.Http; using System; -using System.Collections.Generic; using System.Net; using System.Net.Mail; -using System.Text; using System.Threading.Tasks; -namespace Server.Envir +namespace Server.Infrastructure.Network.Smtp { public static class EmailService { + //TODO: separate SMTP components and game related components (i.e AccountInfo etc). + public static int EMailsSent; public static void SendActivationEmail(AccountInfo account) @@ -40,12 +42,12 @@ public static void SendActivationEmail(AccountInfo account) Body = $"Dear {account.RealName},

" + $"Thank you for registering a Zircon account, before you can log in to the game, you are required to activate your account.

" + $"To complete your registration and activate the account please visit the following link:
" + - $"Click here to Activate

" + + $"Click here to Activate

" + $"If the above link does not work please use the following Activation Key when you next attempt to log in to your account
" + $"Activation Key: {account.ActivationKey}

" + (account.Referral != null ? $"You were referred by: {account.Referral.EMailAddress}

" : "") + $"If you did not create this account and want to cancel the registration to delete this account please visit the following link:
" + - $"Click here to Delete Account

" + + $"Click here to Delete Account

" + $"We'll see you in game
" + $"Zircon Server" }; @@ -180,7 +182,7 @@ public static void SendResetPasswordRequestEmail(AccountInfo account, string ipA $"A request to reset your password has been made.
" + $"IP Address: {ipAddress}

" + $"To reset your password please click on the following link:
" + - $"Reset Password

" + + $"Reset Password

" + $"If the above link does not work please use the following Reset Key to reset your password
" + $"Reset Key: {account.ResetKey}

" + $"If you did not request this reset, please ignore this email as your password will not be changed.

" + diff --git a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs new file mode 100644 index 00000000..6b73c8dc --- /dev/null +++ b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs @@ -0,0 +1,39 @@ +using System; +using System.Net.Sockets; +using System.Text; + +namespace Server.Infrastructure.Network.Tcp.ListenerHandler +{ + public class ActiveUserCountListenerHandler(Func UserCountSupplier) : IListenerHandler + { + public void OnAcceptBegin(TcpListener l, IAsyncResult r) + { + TcpClient client = l.EndAcceptTcpClient(r); + byte[] data = Encoding.ASCII.GetBytes(string.Format("c;/Zircon/{0}/;", UserCountSupplier.Invoke())); + client.Client.BeginSend(data, 0, data.Length, SocketFlags.None, TerminateConnection, client); + } + + public void OnAcceptEnd(TcpListener l, IAsyncResult r) + { + // Do Nothing + } + + public void OnException(Exception ex) + { + // Do Nothing + } + + private void TerminateConnection(IAsyncResult result) + { + try + { + var client = result.AsyncState as TcpClient; + + if (client == null) return; + client.Client.EndSend(result); + client.Client.Dispose(); + } + catch { } + } + } +} diff --git a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs new file mode 100644 index 00000000..38180a8c --- /dev/null +++ b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Sockets; +using System.Text; +using System.Threading.Tasks; + +namespace Server.Infrastructure.Network.Tcp.ListenerHandler +{ + public interface IListenerHandler + { + public void OnAcceptBegin(TcpListener listener, IAsyncResult result); + public void OnAcceptEnd(TcpListener listener, IAsyncResult result); + + public void OnException(Exception ex); + } +} diff --git a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs new file mode 100644 index 00000000..ad9580db --- /dev/null +++ b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs @@ -0,0 +1,28 @@ +using Server.Envir; +using Server.Infrastructure.Service.Connection; +using System; +using System.Net.Sockets; +using System.Threading; + +namespace Server.Infrastructure.Network.Tcp.ListenerHandler +{ + public class UserConnectionListenerHandler(AbstractConnectionService ConnectionService) : IListenerHandler + { + public void OnAcceptBegin(TcpListener listener, IAsyncResult result) + { + ConnectionService.Add(listener.EndAcceptTcpClient(result)); + } + + public void OnAcceptEnd(TcpListener listener, IAsyncResult result) + { + + while (ConnectionService.AcceptingConnections) + Thread.Sleep(1); + } + + public void OnException(Exception ex) + { + SEnvir.Log(ex.ToString()); + } + } +} diff --git a/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs b/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs new file mode 100644 index 00000000..58a12fee --- /dev/null +++ b/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs @@ -0,0 +1,60 @@ +using Server.Envir; +using Server.Infrastructure.Network.Tcp.ListenerHandler; +using System; +using System.Net; +using System.Net.Sockets; + +namespace Server.Infrastructure.Network.Tcp +{ + public class TcpServer(IListenerHandler ListenerHandler, string IpAddress, ushort Port) + { + public bool Started { get; private set; } + + private TcpListener Listener; + + public void Start(bool log = true) + { + try + { + Listener = new TcpListener(IPAddress.Parse(IpAddress), Port); + Listener.Start(); + Listener.BeginAcceptTcpClient(HandleConnection, null); + Started = true; + } + catch (Exception ex) + { + Started = false; + SEnvir.Log(ex.ToString()); + } + } + + public void Stop(bool log = true) + { + TcpListener expiredListener = Listener; + + Listener = null; + Started = false; + expiredListener?.Stop(); + } + + private void HandleConnection(IAsyncResult result) + { + try + { + if (Listener == null || !Listener.Server.IsBound) return; + ListenerHandler.OnAcceptBegin(Listener, result); + } + catch (SocketException) { } + catch (Exception ex) + { + ListenerHandler.OnException(ex); + } + finally + { + ListenerHandler.OnAcceptEnd(Listener, result); + if (Listener != null && Listener.Server.IsBound) + Listener.BeginAcceptTcpClient(HandleConnection, null); + } + } + } +} diff --git a/ServerLibrary/Infrastructure.Network/ConnectionManager.cs b/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs similarity index 75% rename from ServerLibrary/Infrastructure.Network/ConnectionManager.cs rename to ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs index 2d720ce0..9d99c48e 100644 --- a/ServerLibrary/Infrastructure.Network/ConnectionManager.cs +++ b/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs @@ -7,16 +7,16 @@ using System.Net.Sockets; using System.Threading; -namespace Server.Infrastructure.Network +namespace Server.Infrastructure.Service.Connection { - public class ConnectionManager(IConnectionFactory ConnectionFactory, IpAddressManager IpManager) where ConnectionType : BaseConnection + public abstract class AbstractConnectionService(IConnectionFactory ConnectionFactory, IpAddressService IpManager) where ConnectionType : BaseConnection { //public static Dictionary IPCount = new Dictionary(); //TODO: this isn't used but i imagine its intended to limit connections from single IP private bool IsResetting = false; public ConcurrentQueue PendingConnections = []; - public List Connections = []; + public List ActiveConnections = []; public bool AcceptingConnections => PendingConnections?.Count >= 15 || IsResetting; @@ -25,6 +25,9 @@ public class ConnectionManager(IConnectionFactory TotalBytesReceived - PreviousTotalReceived; + public long UploadSpeed => TotalBytesSent - PreviousTotalSent; #endregion internal void Add(TcpClient tcpClient) @@ -45,16 +48,16 @@ internal void Process() //IPCount.TryGetValue(connection.IPAddress, out var ipCount); //IPCount[connection.IPAddress] = ipCount + 1; - Connections.Add(connection); + ActiveConnections.Add(connection); } long bytesSent = 0; long bytesReceived = 0; - for (int i = Connections.Count - 1; i >= 0; i--) + for (int i = ActiveConnections.Count - 1; i >= 0; i--) { - if (i >= Connections.Count) break; + if (i >= ActiveConnections.Count) break; - var connection = Connections[i]; + var connection = ActiveConnections[i]; connection.Process(); if (connection.TotalPacketsProcessed == 0 && connection.TotalBytesReceived > 1024) @@ -65,7 +68,7 @@ internal void Process() return; } - if (connection.ReceiveList.Count > Config.MaxPacket) + if (connection.ReceiveList?.Count > Config.MaxPacket) { connection.TryDisconnect(); IpManager.Timeout(connection, Config.PacketBanTime); @@ -85,22 +88,22 @@ internal void Process() internal void Broadcast(Packet packet) { - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(packet); + for (int i = ActiveConnections.Count - 1; i >= 0; i--) + ActiveConnections[i].SendDisconnect(packet); } internal void Broadcast(Func packet) { - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(packet.Invoke(Connections[i])); + for (int i = ActiveConnections.Count - 1; i >= 0; i--) + ActiveConnections[i].SendDisconnect(packet.Invoke(ActiveConnections[i])); } internal void RemoveConnection(ConnectionType connection) { - if (!Connections.Contains(connection)) + if (!ActiveConnections.Contains(connection)) throw new InvalidOperationException("Connection was not found in list"); //TODO: why do we want to cause an exception here? Why not just log a warning and return? - Connections.Remove(connection); + ActiveConnections.Remove(connection); //IPCount[connection.IPAddress]--; DBytesSent += TotalBytesSent; DBytesReceived += TotalBytesReceived; @@ -114,8 +117,8 @@ internal void Reset() try { Packet p = new Library.Network.GeneralPackets.Disconnect { Reason = DisconnectReason.ServerClosing }; - for (int i = Connections.Count - 1; i >= 0; i--) - Connections[i].SendDisconnect(p); + for (int i = ActiveConnections.Count - 1; i >= 0; i--) + ActiveConnections[i].SendDisconnect(p); Thread.Sleep(2000); // wait for disconnects diff --git a/ServerLibrary/Infrastructure.Network/ConnectionFactory.cs b/ServerLibrary/Infrastructure/Service/Connection/IConnectionFactory.cs similarity index 81% rename from ServerLibrary/Infrastructure.Network/ConnectionFactory.cs rename to ServerLibrary/Infrastructure/Service/Connection/IConnectionFactory.cs index 4e643006..f9849003 100644 --- a/ServerLibrary/Infrastructure.Network/ConnectionFactory.cs +++ b/ServerLibrary/Infrastructure/Service/Connection/IConnectionFactory.cs @@ -1,7 +1,7 @@ using Library.Network; using System.Net.Sockets; -namespace Server.Infrastructure.Network +namespace Server.Infrastructure.Service.Connection { public interface IConnectionFactory where ConnectionType : BaseConnection { diff --git a/ServerLibrary/Infrastructure.Network/SConnection.cs b/ServerLibrary/Infrastructure/Service/Connection/UserConnection.cs similarity index 97% rename from ServerLibrary/Infrastructure.Network/SConnection.cs rename to ServerLibrary/Infrastructure/Service/Connection/UserConnection.cs index a67156a1..9cd8d636 100644 --- a/ServerLibrary/Infrastructure.Network/SConnection.cs +++ b/ServerLibrary/Infrastructure/Service/Connection/UserConnection.cs @@ -14,9 +14,9 @@ using G = Library.Network.GeneralPackets; using S = Library.Network.ServerPackets; -namespace Server.Infrastructure.Network +namespace Server.Infrastructure.Service.Connection { - public sealed class SConnection : BaseConnection + public sealed class UserConnection : BaseConnection { private static int SessionCount; @@ -31,19 +31,17 @@ public sealed class SConnection : BaseConnection public PlayerObject Player { get; set; } public int SessionID { get; } - public SConnection Observed; - public List Observers = new List(); + public UserConnection Observed; + public List Observers = new List(); public List MPSearchResults = new List(); public HashSet VisibleResults = new HashSet(); - //TODO: language should be owned by client, just send a numeric value and let the client translate it. - // new S.SystemMessage { id = 1 }; public StringMessages Language; - public Action DisconnectCallback; + public Action DisconnectCallback; - public SConnection(TcpClient client, Action disconnectCallback) : base(client) + public UserConnection(TcpClient client, Action disconnectCallback) : base(client) { DisconnectCallback = disconnectCallback; @@ -181,7 +179,7 @@ public override void Enqueue(Packet p) if (p == null || !p.ObserverPacket) return; - foreach (SConnection observer in Observers) + foreach (UserConnection observer in Observers) observer.Enqueue(p); } @@ -206,11 +204,11 @@ public void ReceiveChat(string text, MessageType type, List link } } - public void ReceiveChatWithObservers(Func messageFunc, MessageType messageType, List linkedItems = null, uint objectID = 0) + public void ReceiveChatWithObservers(Func messageFunc, MessageType messageType, List linkedItems = null, uint objectID = 0) { ReceiveChat(messageFunc(this), messageType, linkedItems, objectID); - foreach (SConnection observer in Observers) + foreach (UserConnection observer in Observers) observer.ReceiveChat(messageFunc(observer), messageType, linkedItems, objectID); } @@ -596,7 +594,7 @@ public void Process(C.NPCClose p) Player.NPC = null; Player.NPCPage = null; - foreach (SConnection con in Observers) + foreach (UserConnection con in Observers) { con.Enqueue(new S.NPCClose()); } diff --git a/ServerLibrary/Infrastructure/Service/Connection/UserConnectionFactory.cs b/ServerLibrary/Infrastructure/Service/Connection/UserConnectionFactory.cs new file mode 100644 index 00000000..55178cc6 --- /dev/null +++ b/ServerLibrary/Infrastructure/Service/Connection/UserConnectionFactory.cs @@ -0,0 +1,15 @@ +using System; +using System.Net.Sockets; + +namespace Server.Infrastructure.Service.Connection +{ + internal class UserConnectionFactory(Func> disconnectCallback) : IConnectionFactory + { + private readonly Func> DisconnectCallback = disconnectCallback; + + public UserConnection Create(TcpClient tcpClient) + { + return new UserConnection(tcpClient, DisconnectCallback.Invoke()); + } + } +} diff --git a/ServerLibrary/Infrastructure/Service/Connection/UserConnectionService.cs b/ServerLibrary/Infrastructure/Service/Connection/UserConnectionService.cs new file mode 100644 index 00000000..214b5707 --- /dev/null +++ b/ServerLibrary/Infrastructure/Service/Connection/UserConnectionService.cs @@ -0,0 +1,12 @@ +using System; +using System.Linq; + +namespace Server.Infrastructure.Service.Connection +{ + public class UserConnectionService(IConnectionFactory connectionFactory, IpAddressService IpManager) + : AbstractConnectionService(connectionFactory, IpManager) + { + public int Players => ActiveConnections?.Count(c => c.Stage == GameStage.Game) ?? 0; + public int Observers => ActiveConnections?.Count(c => c.Stage == GameStage.Observer) ?? 0; + } +} diff --git a/ServerLibrary/Infrastructure.Network/IpAddressManager.cs b/ServerLibrary/Infrastructure/Service/IpAddressService.cs similarity index 91% rename from ServerLibrary/Infrastructure.Network/IpAddressManager.cs rename to ServerLibrary/Infrastructure/Service/IpAddressService.cs index c849820c..c12c82ae 100644 --- a/ServerLibrary/Infrastructure.Network/IpAddressManager.cs +++ b/ServerLibrary/Infrastructure/Service/IpAddressService.cs @@ -3,9 +3,9 @@ using System; using System.Collections.Generic; -namespace Server.Infrastructure.Network +namespace Server.Infrastructure.Service { - public class IpAddressManager + public class IpAddressService { public Dictionary IpAddressTimeOuts = []; diff --git a/ServerLibrary/Envir/BroadcastService.cs b/ServerLibrary/Infrastructure/Service/UserBroadcastService.cs similarity index 61% rename from ServerLibrary/Envir/BroadcastService.cs rename to ServerLibrary/Infrastructure/Service/UserBroadcastService.cs index 3f1108f4..26c1e89b 100644 --- a/ServerLibrary/Envir/BroadcastService.cs +++ b/ServerLibrary/Infrastructure/Service/UserBroadcastService.cs @@ -1,18 +1,20 @@ using Library; -using Server.Infrastructure.Network; +using Server.Infrastructure.Service.Connection; using System; using System.Collections.Generic; -namespace Server.Envir +namespace Server.Infrastructure.Service { - public class BroadcastService(SConnectionManager ConnectionManager) + public class UserBroadcastService(UserConnectionService UserConnectionService) { - //TODO: dont think this should be here - internal void SendOnlineCount() + //TODO: some of this is convoluted because Language is owned by Server. It should be owned by client server should just send (i.e S.TemplatedMessage { type = 1, args = [...] } and let the client handle local translations + //TODO: not 100% happy - any other improvements? + + internal void BroadcastOnlineCount() { - foreach (SConnection conn in ConnectionManager.Connections) + foreach (UserConnection conn in UserConnectionService.ActiveConnections) { - conn.ReceiveChat(string.Format(conn.Language.OnlineCount, ConnectionManager.Players, ConnectionManager.Observers), MessageType.Hint); + conn.ReceiveChat(string.Format(conn.Language.OnlineCount, UserConnectionService.Players, UserConnectionService.Observers), MessageType.Hint); switch (conn.Stage) { @@ -26,11 +28,9 @@ internal void SendOnlineCount() } } } - - //TODO: dont think this should be here - this is also so convoluted because server owns the languages - internal void BroadcastSystemMessage(Func messageSupplier) + internal void BroadcastSystemMessage(Func messageSupplier) { - foreach (SConnection con in ConnectionManager.Connections) + foreach (UserConnection con in UserConnectionService.ActiveConnections) { switch (con.Stage) { @@ -44,10 +44,9 @@ internal void BroadcastSystemMessage(Func messageSupplier) } } - //TODO: dont think this should be here - internal void BroadcastMessage(string text, List linkedItems, MessageType messageType, Predicate shouldReceive) + internal void BroadcastMessage(string text, List linkedItems, MessageType messageType, Predicate shouldReceive) { - foreach (SConnection con in ConnectionManager.Connections) + foreach (UserConnection con in UserConnectionService.ActiveConnections) { switch (con.Stage) { diff --git a/ServerLibrary/Models/PlayerObject.cs b/ServerLibrary/Models/PlayerObject.cs index 7f85ff35..f2df1762 100644 --- a/ServerLibrary/Models/PlayerObject.cs +++ b/ServerLibrary/Models/PlayerObject.cs @@ -5,7 +5,7 @@ using Server.DBModels; using Server.Envir; using Server.Envir.Events.Triggers; -using Server.Infrastructure.Network; +using Server.Infrastructure.Service.Connection; using Server.Models.Magics; using Server.Models.Monsters; using System; @@ -25,7 +25,7 @@ public sealed class PlayerObject : MapObject public override ObjectType Race => ObjectType.Player; public CharacterInfo Character; - public SConnection Connection; + public UserConnection Connection; public override string Name { @@ -180,7 +180,7 @@ public UserItem[] public Point FishingLocation; public MirDirection FishingDirection; - public PlayerObject(CharacterInfo info, SConnection con) + public PlayerObject(CharacterInfo info, UserConnection con) { Character = info; Connection = con; @@ -1111,7 +1111,7 @@ protected override void OnSpawned() Enqueue(new S.FortuneUpdate { Fortunes = Character.Account.Fortunes.Select(x => x.ToClientInfo()).ToList() }); } - public void SetUpObserver(SConnection con) + public void SetUpObserver(UserConnection con) { con.Stage = GameStage.Observer; con.Observed = Connection; @@ -1494,7 +1494,7 @@ public void Chat(string text) if (parts.Length == 0) return; - SConnection con = SEnvir.GetConnectionByCharacter(parts[0]); + UserConnection con = SEnvir.GetConnectionByCharacter(parts[0]); if (con == null || (con.Stage != GameStage.Observer && con.Stage != GameStage.Game) || SEnvir.IsBlocking(Character.Account, con.Account)) { @@ -1600,7 +1600,7 @@ public void Chat(string text) if (!SEnvir.IsBlocking(Character.Account, player.Character.Account)) player.Connection.ReceiveChat(text, MessageType.Shout, linkedItems); - foreach (SConnection observer in player.Connection.Observers) + foreach (UserConnection observer in player.Connection.Observers) { if (SEnvir.IsBlocking(Character.Account, observer.Account)) continue; @@ -1630,7 +1630,7 @@ public void Chat(string text) Connection.ReceiveChat(text, MessageType.ObserverChat, linkedItems); - foreach (SConnection target in Connection.Observers) + foreach (UserConnection target in Connection.Observers) { if (SEnvir.IsBlocking(Character.Account, target.Account)) continue; @@ -1649,7 +1649,7 @@ public void Chat(string text) if (!SEnvir.IsBlocking(Character.Account, player.Character.Account)) player.Connection.ReceiveChat(text, MessageType.Normal, linkedItems, ObjectID); - foreach (SConnection observer in player.Connection.Observers) + foreach (UserConnection observer in player.Connection.Observers) { if (SEnvir.IsBlocking(Character.Account, observer.Account)) continue; @@ -1658,7 +1658,7 @@ public void Chat(string text) } } } - public void ObserverChat(SConnection con, string text) + public void ObserverChat(UserConnection con, string text) { if (string.IsNullOrEmpty(text)) return; @@ -1679,7 +1679,7 @@ public void ObserverChat(SConnection con, string text) if (parts.Length == 0) return; - SConnection target = SEnvir.GetConnectionByCharacter(parts[0]); + UserConnection target = SEnvir.GetConnectionByCharacter(parts[0]); if (target == null || (target.Stage != GameStage.Observer && target.Stage != GameStage.Game) || SEnvir.IsBlocking(con.Account, target.Account)) { @@ -1755,7 +1755,7 @@ public void ObserverChat(SConnection con, string text) Connection.ReceiveChat(text, MessageType.ObserverChat); - foreach (SConnection target in Connection.Observers) + foreach (UserConnection target in Connection.Observers) { if (SEnvir.IsBlocking(con.Account, target.Account)) continue; @@ -1764,7 +1764,7 @@ public void ObserverChat(SConnection con, string text) } } - public void Inspect(int index, bool ranking, SConnection con) + public void Inspect(int index, bool ranking, UserConnection con) { //if (index == Character.Index) return; @@ -1839,7 +1839,7 @@ public override void ItemRevive() UpdateReviveTimers(Connection); } - public void UpdateReviveTimers(SConnection con) + public void UpdateReviveTimers(UserConnection con) { con.Enqueue(new S.ReviveTimers { @@ -6841,7 +6841,7 @@ public void ItemMove(C.ItemMove p) } else { - foreach (SConnection con in Connection.Observers) + foreach (UserConnection con in Connection.Observers) { con.Enqueue(new S.ItemChanged { @@ -6873,7 +6873,7 @@ public void ItemMove(C.ItemMove p) } else { - foreach (SConnection con in Connection.Observers) + foreach (UserConnection con in Connection.Observers) { con.Enqueue(new S.ItemChanged { @@ -6957,7 +6957,7 @@ public void ItemMove(C.ItemMove p) { //Sendto MY observers I got item from guild store and what slot? - foreach (SConnection con in Connection.Observers) + foreach (UserConnection con in Connection.Observers) { con.Enqueue(new S.GuildGetItem { @@ -7027,7 +7027,7 @@ public void ItemMove(C.ItemMove p) if (p.FromGrid == GridType.GuildStorage) break; //Already Handled //Must be removing from player to GuildStorage, Update Observer's bag - foreach (SConnection con in Connection.Observers) + foreach (UserConnection con in Connection.Observers) { con.Enqueue(new S.ItemChanged { From 6646b8d43ea6886b8be8c7a6ee2a3b1da53cf301 Mon Sep 17 00:00:00 2001 From: Ryan Date: Mon, 4 Aug 2025 22:35:59 +0100 Subject: [PATCH 5/6] Add support ListnerHandler.Termination to allow custom behaviours when stopping tcp server. --- .../Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs | 7 ++++++- .../Network/Tcp/ListenerHandler/IListenerHandler.cs | 3 ++- .../Tcp/ListenerHandler/UserConnectionListenerHandler.cs | 7 ++++++- ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs | 3 ++- .../Service/Connection/AbstractConnectionService.cs | 1 - 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs index 6b73c8dc..ec995b68 100644 --- a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs +++ b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/ActiveUserCountListenerHandler.cs @@ -18,7 +18,12 @@ public void OnAcceptEnd(TcpListener l, IAsyncResult r) // Do Nothing } - public void OnException(Exception ex) + public void OnAcceptException(Exception ex) + { + // Do Nothing + } + + public void OnTermination() { // Do Nothing } diff --git a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs index 38180a8c..2a11dd55 100644 --- a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs +++ b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/IListenerHandler.cs @@ -11,7 +11,8 @@ public interface IListenerHandler { public void OnAcceptBegin(TcpListener listener, IAsyncResult result); public void OnAcceptEnd(TcpListener listener, IAsyncResult result); + public void OnAcceptException(Exception ex); - public void OnException(Exception ex); + public void OnTermination(); } } diff --git a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs index ad9580db..2979c53e 100644 --- a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs +++ b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs @@ -20,9 +20,14 @@ public void OnAcceptEnd(TcpListener listener, IAsyncResult result) Thread.Sleep(1); } - public void OnException(Exception ex) + public void OnAcceptException(Exception ex) { SEnvir.Log(ex.ToString()); } + + public void OnTermination() + { + ConnectionService.Reset(); + } } } diff --git a/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs b/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs index 58a12fee..6f23ee69 100644 --- a/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs +++ b/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs @@ -35,6 +35,7 @@ public void Stop(bool log = true) Listener = null; Started = false; expiredListener?.Stop(); + ListenerHandler.OnTermination(); } private void HandleConnection(IAsyncResult result) @@ -47,7 +48,7 @@ private void HandleConnection(IAsyncResult result) catch (SocketException) { } catch (Exception ex) { - ListenerHandler.OnException(ex); + ListenerHandler.OnAcceptException(ex); } finally { diff --git a/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs b/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs index 9d99c48e..c875a372 100644 --- a/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs +++ b/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs @@ -121,7 +121,6 @@ internal void Reset() ActiveConnections[i].SendDisconnect(p); Thread.Sleep(2000); // wait for disconnects - } catch (Exception ex) { From 9798d4a76a7e317ffa5ab72ef8fbf65b855fba00 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Aug 2025 22:46:05 +0100 Subject: [PATCH 6/6] Moved SEnvir.Log functionality and supporting method into dedicated Logger classes based on individual function. Introduced Scheduler implementation to retain shared thread between logging functions (for resource efficiency) --- Server/Helpers/JsonImporter.cs | 2 +- Server/SMain.cs | 6 +- Server/Views/ChatLogView.cs | 4 +- Server/Views/ConfigView.cs | 4 +- Server/Views/MapViewer.cs | 4 +- Server/Views/SystemLogView.cs | 4 +- ServerLibrary/DBModels/GuildInfo.cs | 2 +- .../Commands/ErrorHandlingCommandHandler.cs | 2 +- ServerLibrary/Envir/Config.cs | 2 +- ServerLibrary/Envir/SEnvir.cs | 206 +++++++----------- .../Infrastructure/Logging/CompositeLogger.cs | 12 + .../Logging/Formatter/ILogFormatter.cs | 8 + .../Logging/Formatter/StdLogFormatter.cs | 12 + .../Infrastructure/Logging/ILogger.cs | 8 + .../Infrastructure/Logging/SystemAppLogger.cs | 15 ++ .../Logging/SystemConsoleLogger.cs | 14 ++ .../Logging/SystemFileLogger.cs | 37 ++++ .../Logging/UserChatAppLogger.cs | 15 ++ .../Logging/UserChatFileLogger.cs | 38 ++++ .../Network/Http/HttpWebServer.cs | 22 +- .../Network/Smtp/EmailService.cs | 20 +- .../UserConnectionListenerHandler.cs | 2 +- .../Infrastructure/Network/Tcp/TcpServer.cs | 6 +- .../Scheduler/RecurringAction.cs | 6 + .../Scheduler/SingleThreadScheduler.cs | 75 +++++++ .../Connection/AbstractConnectionService.cs | 6 +- .../Service/Connection/UserConnection.cs | 8 +- ServerLibrary/Models/Map.cs | 10 +- ServerLibrary/Models/Monsters/CastleGate.cs | 2 +- ServerLibrary/Models/Monsters/Companion.cs | 4 +- ServerLibrary/Models/PlayerObject.cs | 16 +- ServerLibrary/Models/SpellObject.cs | 8 +- 32 files changed, 380 insertions(+), 200 deletions(-) create mode 100644 ServerLibrary/Infrastructure/Logging/CompositeLogger.cs create mode 100644 ServerLibrary/Infrastructure/Logging/Formatter/ILogFormatter.cs create mode 100644 ServerLibrary/Infrastructure/Logging/Formatter/StdLogFormatter.cs create mode 100644 ServerLibrary/Infrastructure/Logging/ILogger.cs create mode 100644 ServerLibrary/Infrastructure/Logging/SystemAppLogger.cs create mode 100644 ServerLibrary/Infrastructure/Logging/SystemConsoleLogger.cs create mode 100644 ServerLibrary/Infrastructure/Logging/SystemFileLogger.cs create mode 100644 ServerLibrary/Infrastructure/Logging/UserChatAppLogger.cs create mode 100644 ServerLibrary/Infrastructure/Logging/UserChatFileLogger.cs create mode 100644 ServerLibrary/Infrastructure/Scheduler/RecurringAction.cs create mode 100644 ServerLibrary/Infrastructure/Scheduler/SingleThreadScheduler.cs diff --git a/Server/Helpers/JsonImporter.cs b/Server/Helpers/JsonImporter.cs index 57ca7f00..eb764a77 100644 --- a/Server/Helpers/JsonImporter.cs +++ b/Server/Helpers/JsonImporter.cs @@ -50,7 +50,7 @@ public static void Import(JsonSerializerOptions options) } catch (Exception ex) { - SEnvir.Log(ex.Message); + SEnvir.ServerLogger.Log(ex.Message); XtraMessageBox.Show($"Failed to import all rows.\r\n\r\n{ex.Message}", "Fail", MessageBoxButtons.OK); } diff --git a/Server/SMain.cs b/Server/SMain.cs index 2c950b23..8e78ceed 100644 --- a/Server/SMain.cs +++ b/Server/SMain.cs @@ -48,7 +48,7 @@ private void SetupPlugin() private void PluginLoader_Log(object sender, PluginCore.LogEventArgs e) { - SEnvir.Log(e.Message); + SEnvir.ServerLogger.Log(e.Message); } private void PluginLoader_ShowView(object sender, ShowViewEventArgs e) @@ -130,7 +130,7 @@ private void Application_Idle(object sender, EventArgs e) } catch (Exception ex) { - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); } } @@ -250,7 +250,7 @@ private void StartServerButton_ItemClick(object sender, DevExpress.XtraBars.Item } catch (Exception ex) { - SEnvir.Log($"Exception: " + ex.ToString(), true); + SEnvir.ServerLogger.Log($"Exception: " + ex.ToString()); } } diff --git a/Server/Views/ChatLogView.cs b/Server/Views/ChatLogView.cs index 7d7edc98..ec6f4195 100644 --- a/Server/Views/ChatLogView.cs +++ b/Server/Views/ChatLogView.cs @@ -18,11 +18,11 @@ public ChatLogView() private void InterfaceTimer_Tick(object sender, EventArgs e) { - while (!SEnvir.ChatLogs.IsEmpty) + while (!SEnvir.ChatAppLogs.IsEmpty) //TODO: not sure if this was a bug or intentional? why was it using SEnvir.ChatLogs instead of ChatDisplayLogs { string log; - if (!SEnvir.DisplayChatLogs.TryDequeue(out log)) continue; + if (!SEnvir.ChatAppLogs.TryDequeue(out log)) continue; Logs.Add(log); } diff --git a/Server/Views/ConfigView.cs b/Server/Views/ConfigView.cs index 07b39322..ac439809 100644 --- a/Server/Views/ConfigView.cs +++ b/Server/Views/ConfigView.cs @@ -38,13 +38,13 @@ private void SyncronizeRemoteButton_Click(object sender, EventArgs e) private void SyncronizeLocalButton_Click(object sender, EventArgs e) { - SEnvir.Log($"Starting local syncronization..."); + SEnvir.ServerLogger.Log($"Starting local syncronization..."); SMain.Session.Save(true); File.Copy(SMain.Session.SystemPath, Path.Combine(Config.ClientPath, "Data\\", Path.GetFileName(SMain.Session.SystemPath)), true); - SEnvir.Log($"Syncronization completed..."); + SEnvir.ServerLogger.Log($"Syncronization completed..."); } protected override void OnLoad(EventArgs e) diff --git a/Server/Views/MapViewer.cs b/Server/Views/MapViewer.cs index 146f138b..a7772d10 100644 --- a/Server/Views/MapViewer.cs +++ b/Server/Views/MapViewer.cs @@ -205,7 +205,7 @@ private void RenderEnvironment() } catch (Exception ex) { - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); Manager.AttemptRecovery(); } @@ -2152,7 +2152,7 @@ public void Load(string fileName) } catch (Exception ex) { - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); } TextureValid = false; } diff --git a/Server/Views/SystemLogView.cs b/Server/Views/SystemLogView.cs index c7013549..2e50a120 100644 --- a/Server/Views/SystemLogView.cs +++ b/Server/Views/SystemLogView.cs @@ -18,11 +18,11 @@ public SystemLogView() private void InterfaceTimer_Tick(object sender, EventArgs e) { - while (!SEnvir.DisplayLogs.IsEmpty) + while (!SEnvir.ServerAppLogs.IsEmpty) { string log; - if (!SEnvir.DisplayLogs.TryDequeue(out log)) continue; + if (!SEnvir.ServerAppLogs.TryDequeue(out log)) continue; Logs.Add(log); } diff --git a/ServerLibrary/DBModels/GuildInfo.cs b/ServerLibrary/DBModels/GuildInfo.cs index a5460b1b..b5ddc645 100644 --- a/ServerLibrary/DBModels/GuildInfo.cs +++ b/ServerLibrary/DBModels/GuildInfo.cs @@ -319,7 +319,7 @@ protected override void OnLoaded() { if (item.Slot < 0 || item.Slot >= Storage.Length) { - SEnvir.Log(string.Format("[BAD ITEM] Guild: {0}, Slot: {1}", GuildName, item.Slot)); + SEnvir.ServerLogger.Log(string.Format("[BAD ITEM] Guild: {0}, Slot: {1}", GuildName, item.Slot)); continue; } diff --git a/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs b/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs index b1b4dde4..1777b7cc 100644 --- a/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs +++ b/ServerLibrary/Envir/Commands/ErrorHandlingCommandHandler.cs @@ -48,7 +48,7 @@ public void Handle(PlayerObject player, string[] commandParts) } catch (Exception exception) { - SEnvir.Log("FatalCommandError [" + player.Name + "]: " + exception.Message); + SEnvir.ServerLogger.Log("FatalCommandError [" + player.Name + "]: " + exception.Message); player.Connection.ReceiveChat("FatalCommandError: The error has been logged. Contact an admin.", MessageType.System); } } diff --git a/ServerLibrary/Envir/Config.cs b/ServerLibrary/Envir/Config.cs index a0c9e84d..f15a3868 100644 --- a/ServerLibrary/Envir/Config.cs +++ b/ServerLibrary/Envir/Config.cs @@ -160,7 +160,7 @@ public static void LoadVersion() } catch (Exception ex) { - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); } } } diff --git a/ServerLibrary/Envir/SEnvir.cs b/ServerLibrary/Envir/SEnvir.cs index 17bc3888..10690b34 100644 --- a/ServerLibrary/Envir/SEnvir.cs +++ b/ServerLibrary/Envir/SEnvir.cs @@ -7,10 +7,13 @@ using Server.Envir.Commands.Handler; using Server.Envir.Events; using Server.Envir.Events.Triggers; +using Server.Infrastructure.Logging; +using Server.Infrastructure.Logging.Formatter; using Server.Infrastructure.Network.Http; using Server.Infrastructure.Network.Smtp; using Server.Infrastructure.Network.Tcp; using Server.Infrastructure.Network.Tcp.ListenerHandler; +using Server.Infrastructure.Scheduler; using Server.Infrastructure.Service; using Server.Infrastructure.Service.Connection; using Server.Models; @@ -34,62 +37,45 @@ namespace Server.Envir { public static class SEnvir { - #region Logging - - public static ConcurrentQueue DisplayLogs = []; - public static ConcurrentQueue Logs = []; - public static bool UseLogConsole = false; - - public static void Log(string log, bool hardLog = true) - { - log = string.Format("[{0:F}]: {1}", Time.Now, log); - - if (UseLogConsole) - { - Console.WriteLine(log); - } - else - { - if (DisplayLogs.Count < 100) - DisplayLogs.Enqueue(log); - - if (hardLog && Logs.Count < 1000) - Logs.Enqueue(log); - } - } - - public static ConcurrentQueue DisplayChatLogs = new ConcurrentQueue(); - public static ConcurrentQueue ChatLogs = new ConcurrentQueue(); - public static void LogChat(string log) - { - log = string.Format("[{0:F}]: {1}", Time.Now, log); - - if (DisplayChatLogs.Count < 500) - DisplayChatLogs.Enqueue(log); - - if (ChatLogs.Count < 1000) - ChatLogs.Enqueue(log); - } - - #endregion + //TODO: EVERYTHING static in here should be using DI ideally. public static readonly IpAddressService IpManager = new IpAddressService(); public static readonly UserConnectionService UserConnectionService = new UserConnectionService( - new UserConnectionFactory(() => UserConnectionService.RemoveConnection), + new UserConnectionFactory(() => UserConnectionService.RemoveConnection), IpManager ); + public static readonly UserBroadcastService BroadcastService = new UserBroadcastService(UserConnectionService); + + #region Logging + //TODO: these things should not exist here - need to move to DI then we can push this up outside of SEnvir + public static bool UseLogConsole = false; + public static readonly ConcurrentQueue ServerAppLogs = []; + public static readonly ConcurrentQueue ChatAppLogs = []; + //TODO-END + + private static SingleThreadScheduler LogWritingScheduler = new SingleThreadScheduler(); + private static readonly ILogFormatter LogFormatter = new StdLogFormatter(); + public static readonly ILogger ServerLogger = new CompositeLogger( + UseLogConsole ? [new SystemConsoleLogger(LogFormatter)] : [new SystemAppLogger(ServerAppLogs, LogFormatter), new SystemFileLogger(LogWritingScheduler, LogFormatter)] + ); + public static readonly ILogger UserChatLogger = new CompositeLogger( + UseLogConsole ? [new UserChatFileLogger(LogWritingScheduler,LogFormatter)] : [new UserChatAppLogger(ChatAppLogs, LogFormatter), new UserChatFileLogger(LogWritingScheduler, LogFormatter)] + ); + #endregion - public static readonly TcpServer UserTcpServer = new TcpServer( + #region TcpServer + private static readonly TcpServer UserTcpServer = new TcpServer( new UserConnectionListenerHandler(UserConnectionService), Config.IPAddress, Config.Port ); - public static readonly TcpServer ActiveUserCountTcpServer = new TcpServer( + + private static readonly TcpServer ActiveUserCountTcpServer = new TcpServer( new ActiveUserCountListenerHandler(() => UserConnectionService.ActiveConnections.Count), Config.IPAddress, Config.UserCountPort ); - public static readonly UserBroadcastService BroadcastService = new UserBroadcastService(UserConnectionService); + #endregion public static bool Started //TODO: not sure what purpose of this was really.. it was 100% controlled by UserTcpServer { @@ -292,11 +278,11 @@ public static void LoadExperienceList() } catch (Exception) { - Log(string.Format("ExperienceList: Error parsing line {0} - {1}", i, lines[i])); + ServerLogger.Log(string.Format("ExperienceList: Error parsing line {0} - {1}", i, lines[i])); } } } - Log("Experience List Loaded."); + ServerLogger.Log("Experience List Loaded."); } private static void LoadDatabase() @@ -593,13 +579,13 @@ private static void CreateMovements(InstanceInfo instance = null, byte instanceS { if (movement.SourceRegion == null && movement.DestinationRegion == null) { - Log($"[Movement] No Source or Destination Region, Index: {movement.Index}"); + ServerLogger.Log($"[Movement] No Source or Destination Region, Index: {movement.Index}"); continue; } if (movement.SourceRegion == null) { - Log($"[Movement] No Source Region, Destination: {movement.DestinationRegion.ServerDescription}"); + ServerLogger.Log($"[Movement] No Source Region, Destination: {movement.DestinationRegion.ServerDescription}"); continue; } @@ -609,7 +595,7 @@ private static void CreateMovements(InstanceInfo instance = null, byte instanceS { if (instance == null) { - Log($"[Movement] Bad Source Map, Source: {movement.SourceRegion.ServerDescription}"); + ServerLogger.Log($"[Movement] Bad Source Map, Source: {movement.SourceRegion.ServerDescription}"); } continue; @@ -617,13 +603,13 @@ private static void CreateMovements(InstanceInfo instance = null, byte instanceS if (movement.DestinationRegion == null) { - Log($"[Movement] No Destination Region, Source: {movement.SourceRegion.ServerDescription}"); + ServerLogger.Log($"[Movement] No Destination Region, Source: {movement.SourceRegion.ServerDescription}"); continue; } if (movement.DestinationRegion.PointList.Count == 0) { - Log($"[Movement] Bad Destination, Dest: {movement.DestinationRegion.ServerDescription}, No Points"); + ServerLogger.Log($"[Movement] Bad Destination, Dest: {movement.DestinationRegion.ServerDescription}, No Points"); continue; } @@ -633,7 +619,7 @@ private static void CreateMovements(InstanceInfo instance = null, byte instanceS { if (instance == null) { - Log($"[Movement] Bad Destination Map, Destination: {movement.DestinationRegion.ServerDescription}"); + ServerLogger.Log($"[Movement] Bad Destination Map, Destination: {movement.DestinationRegion.ServerDescription}"); } if (movement.NeedInstance == null || movement.NeedInstance != instance) @@ -648,7 +634,7 @@ private static void CreateMovements(InstanceInfo instance = null, byte instanceS if (source == null) { - Log($"[Movement] Bad Origin, Source: {movement.SourceRegion.ServerDescription}, X:{sPoint.X}, Y:{sPoint.Y}"); + ServerLogger.Log($"[Movement] Bad Origin, Source: {movement.SourceRegion.ServerDescription}, X:{sPoint.X}, Y:{sPoint.Y}"); continue; } @@ -672,7 +658,7 @@ private static void CreateNPCs(InstanceInfo instance = null, byte instanceSequen { if (instance == null) { - Log(string.Format("[NPC] Bad Map, NPC: {0}, Map: {1}", info.NPCName, info.Region.ServerDescription)); + ServerLogger.Log(string.Format("[NPC] Bad Map, NPC: {0}, Map: {1}", info.NPCName, info.Region.ServerDescription)); } continue; @@ -684,7 +670,7 @@ private static void CreateNPCs(InstanceInfo instance = null, byte instanceSequen }; if (!ob.Spawn(info.Region, instance, instanceSequence)) - Log($"[NPC] Failed to spawn NPC, Region: {info.Region.ServerDescription}, NPC: {info.NPCName}"); + ServerLogger.Log($"[NPC] Failed to spawn NPC, Region: {info.Region.ServerDescription}, NPC: {info.NPCName}"); } } @@ -703,7 +689,7 @@ private static void CreateQuestRegions(InstanceInfo instance = null, byte instan { if (instance == null) { - Log($"[Quest Region] Bad Map, Map: {task.RegionParameter.ServerDescription}"); + ServerLogger.Log($"[Quest Region] Bad Map, Map: {task.RegionParameter.ServerDescription}"); } continue; @@ -715,7 +701,7 @@ private static void CreateQuestRegions(InstanceInfo instance = null, byte instan if (source == null) { - Log($"[Quest Region] Bad Quest Region, Source: {task.RegionParameter.ServerDescription}, X:{sPoint.X}, Y:{sPoint.Y}"); + ServerLogger.Log($"[Quest Region] Bad Quest Region, Source: {task.RegionParameter.ServerDescription}, X:{sPoint.X}, Y:{sPoint.Y}"); continue; } @@ -742,7 +728,7 @@ private static void CreateSafeZones(InstanceInfo instance = null, byte instanceS { if (instance == null) { - Log($"[Safe Zone] Bad Map, Map: {info.Region.ServerDescription}"); + ServerLogger.Log($"[Safe Zone] Bad Map, Map: {info.Region.ServerDescription}"); } continue; @@ -758,7 +744,7 @@ private static void CreateSafeZones(InstanceInfo instance = null, byte instanceS if (cell == null) { - Log($"[Safe Zone] Bad Location, Region: {info.Region.ServerDescription}, X: {point.X}, Y: {point.Y}."); + ServerLogger.Log($"[Safe Zone] Bad Location, Region: {info.Region.ServerDescription}, X: {point.X}, Y: {point.Y}."); continue; } @@ -800,7 +786,7 @@ private static void CreateSafeZones(InstanceInfo instance = null, byte instanceS if (map == null) { - Log($"[Safe Zone] Bad Bind Map, Map: {info.Region.ServerDescription}"); + ServerLogger.Log($"[Safe Zone] Bad Bind Map, Map: {info.Region.ServerDescription}"); continue; } @@ -811,7 +797,7 @@ private static void CreateSafeZones(InstanceInfo instance = null, byte instanceS if (cell == null) { - Log($"[Safe Zone] Bad Location, Region: {info.BindRegion.ServerDescription}, X: {point.X}, Y: {point.Y}."); + ServerLogger.Log($"[Safe Zone] Bad Location, Region: {info.BindRegion.ServerDescription}, X: {point.X}, Y: {point.Y}."); continue; } @@ -833,7 +819,7 @@ private static void CreateSpawns(InstanceInfo instance = null, byte instanceSequ { if (instance == null) { - Log(string.Format("[Respawn] Bad Map, Map: {0}", info.Region.ServerDescription)); + ServerLogger.Log(string.Format("[Respawn] Bad Map, Map: {0}", info.Region.ServerDescription)); } continue; @@ -922,20 +908,18 @@ public static void EnvirLoop() StartEnvir(); UserTcpServer.Start(); + ServerLogger.Log($"Network Started. Listen: {Config.IPAddress}:{Config.Port}"); //TODO: maybe pass this into Start() method as a PostStartupHook param ActiveUserCountTcpServer.Start(); - SEnvir.Log($"Network Started. Listen: {Config.IPAddress}:{Config.Port}"); HttpWebServer.StartWebServer(); int count = 0, loopCount = 0; DateTime nextCount = Now.AddSeconds(1), UserCountTime = Now.AddMinutes(5), EventTimerTime = Now.AddMinutes(1), saveTime; int lastindex = 0; long conDelay = 0; - Thread logThread = new Thread(WriteLogsLoop) { IsBackground = true }; - logThread.Start(); LastWarTime = Now; - Log($"Loading Time: {Functions.ToString(Time.Now - Now, true)}"); + ServerLogger.Log($"Loading Time: {Functions.ToString(Time.Now - Now, true)}"); while (Started) { @@ -987,8 +971,8 @@ public static void EnvirLoop() ActiveObjects.Remove(ob); ob.Activated = false; - Log(ex.Message); - Log(ex.StackTrace); + ServerLogger.Log(ex.Message); + ServerLogger.Log(ex.StackTrace); File.AppendAllText(@".\Errors.txt", ex.StackTrace + Environment.NewLine); } } @@ -1112,8 +1096,8 @@ public static void EnvirLoop() { Session = null; - Log(ex.Message); - Log(ex.StackTrace); + ServerLogger.Log(ex.Message); + ServerLogger.Log(ex.StackTrace); File.AppendAllText(@".\Errors.txt", ex.StackTrace + Environment.NewLine); UserConnectionService.Broadcast(new G.Disconnect { Reason = DisconnectReason.Crashed }); @@ -1126,7 +1110,7 @@ public static void EnvirLoop() HttpWebServer.StopWebServer(); UserTcpServer.Stop(); ActiveUserCountTcpServer.Stop(); - SEnvir.Log("Network Stopped."); + SEnvir.ServerLogger.Log("Network Stopped."); while (Saving) Thread.Sleep(1); if (Session != null) @@ -1149,7 +1133,7 @@ public static void ProcessGameGold() if (!message.Verified) { - SEnvir.Log("INVALID PAYPAL TRANSACTION " + message.Message); + SEnvir.ServerLogger.Log("INVALID PAYPAL TRANSACTION " + message.Message); continue; } @@ -1185,7 +1169,7 @@ public static void ProcessGameGold() if (SEnvir.GameGoldPaymentList[i].Status != paymentStatus) continue; - SEnvir.Log(string.Format("[Duplicated Transaction] ID:{0} Status:{1}.", transactionID, paymentStatus)); + SEnvir.ServerLogger.Log(string.Format("[Duplicated Transaction] ID:{0} Status:{1}.", transactionID, paymentStatus)); message.Duplicate = true; return; } @@ -1260,7 +1244,7 @@ public static void ProcessGameGold() if (character == null || payment.Error) { - SEnvir.Log($"[Transaction Error] ID:{transactionID} Status:{paymentStatus}, Amount{payment.Price}."); + SEnvir.ServerLogger.Log($"[Transaction Error] ID:{transactionID} Status:{paymentStatus}, Amount{payment.Price}."); continue; } @@ -1282,7 +1266,7 @@ public static void ProcessGameGold() } } - SEnvir.Log($"[Game Gold Purchase] Character: {character.CharacterName}, Amount: {payment.GameGoldAmount}."); + SEnvir.ServerLogger.Log($"[Game Gold Purchase] Character: {character.CharacterName}, Amount: {payment.GameGoldAmount}."); } } @@ -1307,50 +1291,6 @@ private static void CommitChanges(object data) Saving = false; } - private static void WriteLogsLoop() - { - DateTime NextLogTime = Now.AddSeconds(10); - - while (Started) - { - if (Now < NextLogTime) - { - Thread.Sleep(1); - continue; - } - - WriteLogs(); - - NextLogTime = Now.AddSeconds(10); - } - } - private static void WriteLogs() - { - var logPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".\\Logs.txt")); - var chatLogPath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".\\Chat Logs.txt")); - - List lines = new List(); - while (!Logs.IsEmpty) - { - if (!Logs.TryDequeue(out string line)) continue; - lines.Add(line); - } - - File.AppendAllLines(logPath, lines); - - lines.Clear(); - - while (!ChatLogs.IsEmpty) - { - if (!ChatLogs.TryDequeue(out string line)) continue; - lines.Add(line); - } - - File.AppendAllLines(chatLogPath, lines); - - lines.Clear(); - } - public static void CheckGuildWars() { TimeSpan change = Now - LastWarTime; @@ -2605,7 +2545,7 @@ public static void Login(C.Login p, UserConnection con) { account = GetCharacter(p.EMailAddress)?.Account; admin = true; - Log($"[Admin Attempted] Character: {p.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Admin Attempted] Character: {p.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } else { @@ -2663,7 +2603,7 @@ public static void Login(C.Login p, UserConnection con) if (!admin && !PasswordMatch(p.Password, account.Password)) { - Log($"[Wrong Password] IP Address: {con.IPAddress}, Account: {account.EMailAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Wrong Password] IP Address: {con.IPAddress}, Account: {account.EMailAddress}, Security: {p.CheckSum}"); if (account.WrongPasswordCount++ >= 5) { @@ -2693,7 +2633,7 @@ public static void Login(C.Login p, UserConnection con) // account.Connection.SendDisconnect(new G.Disconnect { Reason = DisconnectReason.AnotherUserAdmin }); } - Log($"[Account in Use] Account: {account.EMailAddress}, Current IP: {account.LastIP}, New IP: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Account in Use] Account: {account.EMailAddress}, Current IP: {account.LastIP}, New IP: {con.IPAddress}, Security: {p.CheckSum}"); if (account.TempAdmin) { @@ -2752,7 +2692,7 @@ public static void Login(C.Login p, UserConnection con) account.LastSum = p.CheckSum; } - Log($"[Account Logon] Admin: {admin}, Account: {account.EMailAddress}, IP Address: {account.LastIP}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Account Logon] Admin: {admin}, Account: {account.EMailAddress}, IP Address: {account.LastIP}, Security: {p.CheckSum}"); } public static void NewAccount(C.NewAccount p, UserConnection con) { @@ -2806,7 +2746,7 @@ public static void NewAccount(C.NewAccount p, UserConnection con) if (nowcount > 2 || todaycount > 5) { IpManager.Timeout(con, TimeSpan.FromDays(7)); - Log($"{con.IPAddress} Disconnected and banned for trying too many accounts"); + ServerLogger.Log($"{con.IPAddress} Disconnected and banned for trying too many accounts"); return; } @@ -2872,7 +2812,7 @@ public static void NewAccount(C.NewAccount p, UserConnection con) con.Enqueue(new S.NewAccount { Result = NewAccountResult.Success }); - Log($"[Account Created] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Account Created] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } public static void ChangePassword(C.ChangePassword p, UserConnection con) { @@ -2935,7 +2875,7 @@ public static void ChangePassword(C.ChangePassword p, UserConnection con) if (!PasswordMatch(p.CurrentPassword, account.Password)) { - Log($"[Wrong Password] IP Address: {con.IPAddress}, Account: {account.EMailAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Wrong Password] IP Address: {con.IPAddress}, Account: {account.EMailAddress}, Security: {p.CheckSum}"); if (account.WrongPasswordCount++ >= 5) { @@ -2955,7 +2895,7 @@ public static void ChangePassword(C.ChangePassword p, UserConnection con) EmailService.SendChangePasswordEmail(account, con.IPAddress); con.Enqueue(new S.ChangePassword { Result = ChangePasswordResult.Success }); - Log($"[Password Changed] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Password Changed] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } public static void RequestPasswordReset(C.RequestPasswordReset p, UserConnection con) { @@ -3000,7 +2940,7 @@ public static void RequestPasswordReset(C.RequestPasswordReset p, UserConnection EmailService.SendResetPasswordRequestEmail(account, con.IPAddress); con.Enqueue(new S.RequestPasswordReset { Result = RequestPasswordResetResult.Success }); - Log($"[Request Password] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Request Password] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } public static void ResetPassword(C.ResetPassword p, UserConnection con) { @@ -3043,7 +2983,7 @@ public static void ResetPassword(C.ResetPassword p, UserConnection con) EmailService.SendChangePasswordEmail(account, con.IPAddress); con.Enqueue(new S.ResetPassword { Result = ResetPasswordResult.Success }); - Log($"[Reset Password] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Reset Password] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } public static void Activation(C.Activation p, UserConnection con) { @@ -3072,7 +3012,7 @@ public static void Activation(C.Activation p, UserConnection con) con.Enqueue(new S.Activation { Result = ActivationResult.Success }); - Log($"[Activation] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Activation] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } public static void RequestActivationKey(C.RequestActivationKey p, UserConnection con) { @@ -3115,7 +3055,7 @@ public static void RequestActivationKey(C.RequestActivationKey p, UserConnection } EmailService.ResendActivationEmail(account); con.Enqueue(new S.RequestActivationKey { Result = RequestActivationKeyResult.Success }); - Log($"[Request Activation] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Request Activation] Account: {account.EMailAddress}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } public static void NewCharacter(C.NewCharacter p, UserConnection con) @@ -3273,7 +3213,7 @@ public static void NewCharacter(C.NewCharacter p, UserConnection con) Character = cInfo.ToSelectInfo(), }); - Log($"[Character Created] Character: {p.CharacterName}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Character Created] Character: {p.CharacterName}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); } public static void DeleteCharacter(C.DeleteCharacter p, UserConnection con) { @@ -3296,7 +3236,7 @@ public static void DeleteCharacter(C.DeleteCharacter p, UserConnection con) character.Deleted = true; con.Enqueue(new S.DeleteCharacter { Result = DeleteCharacterResult.Success, DeletedIndex = character.Index }); - Log($"[Character Deleted] Character: {character.CharacterName}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); + ServerLogger.Log($"[Character Deleted] Character: {character.CharacterName}, IP Address: {con.IPAddress}, Security: {p.CheckSum}"); return; } @@ -3572,7 +3512,7 @@ public static Map GetMap(MapInfo info, InstanceInfo instance = null, byte instan CreateQuestRegions(instance, instanceSequence); - Log($"Loaded Instance {instance.Name} at index {instanceSequence}"); + ServerLogger.Log($"Loaded Instance {instance.Name} at index {instanceSequence}"); return instanceSequence; } @@ -3646,7 +3586,7 @@ public static void UnloadInstance(InstanceInfo instance, byte instanceSequence) } } - Log($"Unloaded Instance {instance.Name} at index {instanceSequence} and removed {users.Count} user records"); + ServerLogger.Log($"Unloaded Instance {instance.Name} at index {instanceSequence} and removed {users.Count} user records"); } public static UserConquestStats GetConquestStats(PlayerObject player) diff --git a/ServerLibrary/Infrastructure/Logging/CompositeLogger.cs b/ServerLibrary/Infrastructure/Logging/CompositeLogger.cs new file mode 100644 index 00000000..10578516 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/CompositeLogger.cs @@ -0,0 +1,12 @@ + +namespace Server.Infrastructure.Logging +{ + internal class CompositeLogger(params ILogger[] DelegateLoggers) : ILogger + { + public void Log(string message) + { + foreach (var logger in DelegateLoggers) + logger.Log(message); + } + } +} diff --git a/ServerLibrary/Infrastructure/Logging/Formatter/ILogFormatter.cs b/ServerLibrary/Infrastructure/Logging/Formatter/ILogFormatter.cs new file mode 100644 index 00000000..42f53a15 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/Formatter/ILogFormatter.cs @@ -0,0 +1,8 @@ + +namespace Server.Infrastructure.Logging.Formatter +{ + public interface ILogFormatter + { + string Format(string message); + } +} diff --git a/ServerLibrary/Infrastructure/Logging/Formatter/StdLogFormatter.cs b/ServerLibrary/Infrastructure/Logging/Formatter/StdLogFormatter.cs new file mode 100644 index 00000000..3efd0199 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/Formatter/StdLogFormatter.cs @@ -0,0 +1,12 @@ +using Library; + +namespace Server.Infrastructure.Logging.Formatter +{ + public class StdLogFormatter : ILogFormatter + { + public string Format(string message) + { + return string.Format("[{0:F}]: {1}", Time.Now, message); + } + } +} diff --git a/ServerLibrary/Infrastructure/Logging/ILogger.cs b/ServerLibrary/Infrastructure/Logging/ILogger.cs new file mode 100644 index 00000000..d95f38d3 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/ILogger.cs @@ -0,0 +1,8 @@ + +namespace Server.Infrastructure.Logging +{ + public interface ILogger + { + void Log(string message); + } +} diff --git a/ServerLibrary/Infrastructure/Logging/SystemAppLogger.cs b/ServerLibrary/Infrastructure/Logging/SystemAppLogger.cs new file mode 100644 index 00000000..2bb87ed7 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/SystemAppLogger.cs @@ -0,0 +1,15 @@ +using Server.Infrastructure.Logging.Formatter; +using System.Collections.Concurrent; + +namespace Server.Infrastructure.Logging +{ + //TODO: this should ideally be owned by Server and injected + internal class SystemAppLogger(ConcurrentQueue Logs, ILogFormatter LogFormatter) : ILogger + { + public void Log(string message) + { + if (Logs.Count < 100) + Logs.Enqueue(LogFormatter.Format(message)); + } + } +} diff --git a/ServerLibrary/Infrastructure/Logging/SystemConsoleLogger.cs b/ServerLibrary/Infrastructure/Logging/SystemConsoleLogger.cs new file mode 100644 index 00000000..8e599db9 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/SystemConsoleLogger.cs @@ -0,0 +1,14 @@ +using Server.Infrastructure.Logging.Formatter; +using System; + +namespace Server.Infrastructure.Logging +{ + //TODO: this should ideally be owned by ServerCore and injected + public class SystemConsoleLogger(ILogFormatter LogFormatter) : ILogger + { + public void Log(string message) + { + Console.WriteLine(LogFormatter.Format(message)); + } + } +} diff --git a/ServerLibrary/Infrastructure/Logging/SystemFileLogger.cs b/ServerLibrary/Infrastructure/Logging/SystemFileLogger.cs new file mode 100644 index 00000000..b4a6fb20 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/SystemFileLogger.cs @@ -0,0 +1,37 @@ +using Server.Infrastructure.Logging.Formatter; +using Server.Infrastructure.Scheduler; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; + +namespace Server.Infrastructure.Logging +{ + internal class SystemFileLogger : ILogger + { + private static readonly string LOG_PATH = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".\\Logs.txt")); + + private readonly ILogFormatter LogFormatter; + private readonly ConcurrentQueue Logs = []; + + public SystemFileLogger(SingleThreadScheduler scheduler, ILogFormatter logFormatter) + { + LogFormatter = logFormatter; + scheduler.ScheduleRecurring(WriteLogs, TimeSpan.FromSeconds(10)); + } + + public void Log(string message) + { + if (Logs.Count < 1000) //hardLog && - this is never used its always using default of true + Logs.Enqueue(LogFormatter.Format(message)); + } + + private void WriteLogs() + { + List lines = []; + while (Logs.TryDequeue(out string line)) + lines.Add(line); + File.AppendAllLines(LOG_PATH, lines); + } + } +} diff --git a/ServerLibrary/Infrastructure/Logging/UserChatAppLogger.cs b/ServerLibrary/Infrastructure/Logging/UserChatAppLogger.cs new file mode 100644 index 00000000..8f0dcb0a --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/UserChatAppLogger.cs @@ -0,0 +1,15 @@ +using Server.Infrastructure.Logging.Formatter; +using System.Collections.Concurrent; + +namespace Server.Infrastructure.Logging +{ + //TODO: this should ideally be owned by Server and injected + internal class UserChatAppLogger(ConcurrentQueue Logs, ILogFormatter LogFormatter) : ILogger + { + public void Log(string message) + { + if (Logs.Count < 500) + Logs.Enqueue(LogFormatter.Format(message)); + } + } +} diff --git a/ServerLibrary/Infrastructure/Logging/UserChatFileLogger.cs b/ServerLibrary/Infrastructure/Logging/UserChatFileLogger.cs new file mode 100644 index 00000000..bbef5034 --- /dev/null +++ b/ServerLibrary/Infrastructure/Logging/UserChatFileLogger.cs @@ -0,0 +1,38 @@ +using Server.Envir; +using Server.Infrastructure.Logging.Formatter; +using Server.Infrastructure.Scheduler; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; + +namespace Server.Infrastructure.Logging +{ + internal class UserChatFileLogger : ILogger + { + private readonly string LOG_PATH = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ".\\Chat Logs.txt")); + + private readonly ILogFormatter LogFormatter; + private readonly ConcurrentQueue Logs = []; + + public UserChatFileLogger(SingleThreadScheduler scheduler, ILogFormatter logFormatter) + { + LogFormatter = logFormatter; + scheduler.ScheduleRecurring(WriteLogs, TimeSpan.FromSeconds(10)); + } + + public void Log(string message) + { + if (Logs.Count < 1000) + Logs.Enqueue(LogFormatter.Format(message)); + } + + private void WriteLogs() + { + List lines = []; + while (Logs.TryDequeue(out string line)) + lines.Add(line); + File.AppendAllLines(LOG_PATH, lines); + } + } +} diff --git a/ServerLibrary/Infrastructure/Network/Http/HttpWebServer.cs b/ServerLibrary/Infrastructure/Network/Http/HttpWebServer.cs index 9f6d716f..85259344 100644 --- a/ServerLibrary/Infrastructure/Network/Http/HttpWebServer.cs +++ b/ServerLibrary/Infrastructure/Network/Http/HttpWebServer.cs @@ -92,12 +92,12 @@ public static void StartWebServer(bool log = true) WebServerStarted = true; - if (log) SEnvir.Log("Web Server Started."); + if (log) SEnvir.ServerLogger.Log("Web Server Started."); } catch (Exception ex) { WebServerStarted = false; - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); if (WebListener != null && WebListener.IsListening) WebListener?.Stop(); @@ -130,7 +130,7 @@ public static void StopWebServer(bool log = true) expiredBuyListener?.Stop(); expiredIPNListener?.Stop(); - if (log) SEnvir.Log("Web Server Stopped."); + if (log) SEnvir.ServerLogger.Log("Web Server Stopped."); } private static void WebConnection(IAsyncResult result) @@ -171,21 +171,21 @@ private static void SystemDBSync(HttpListenerContext context) { if (!Config.AllowSystemDBSync) { - SEnvir.Log($"Trying sync but not enabled"); + SEnvir.ServerLogger.Log($"Trying sync but not enabled"); context.Response.StatusCode = 401; return; } if (context.Request.HttpMethod != "POST" || !context.Request.HasEntityBody) { - SEnvir.Log($"Trying sync but method is not post or not have body"); + SEnvir.ServerLogger.Log($"Trying sync but method is not post or not have body"); context.Response.StatusCode = 401; return; } if (context.Request.ContentLength64 > 1024 * 1024 * 10) { - SEnvir.Log($"Trying sync but exceeded SystemDB size"); + SEnvir.ServerLogger.Log($"Trying sync but exceeded SystemDB size"); context.Response.StatusCode = 400; return; } @@ -193,12 +193,12 @@ private static void SystemDBSync(HttpListenerContext context) var masterPassword = context.Request.QueryString["Key"]; if (string.IsNullOrEmpty(masterPassword) || !masterPassword.Equals(Config.SyncKey)) { - SEnvir.Log($"Trying sync but key received is not valid"); + SEnvir.ServerLogger.Log($"Trying sync but key received is not valid"); context.Response.StatusCode = 400; return; } - SEnvir.Log($"Starting remote syncronization..."); + SEnvir.ServerLogger.Log($"Starting remote syncronization..."); var buffer = new byte[context.Request.ContentLength64]; var offset = 0; @@ -228,7 +228,7 @@ private static void SystemDBSync(HttpListenerContext context) context.Response.StatusCode = 200; - SEnvir.Log($"Syncronization completed..."); + SEnvir.ServerLogger.Log($"Syncronization completed..."); } catch (Exception ex) { @@ -236,7 +236,7 @@ private static void SystemDBSync(HttpListenerContext context) context.Response.ContentType = "text/plain"; var message = Encoding.UTF8.GetBytes(ex.ToString()); context.Response.OutputStream.Write(message, 0, message.Length); - SEnvir.Log("Syncronization exception: " + ex.ToString()); + SEnvir.ServerLogger.Log("Syncronization exception: " + ex.ToString()); } finally { @@ -403,7 +403,7 @@ private static void IPNConnection(IAsyncResult result) } catch (Exception ex) { - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); } finally { diff --git a/ServerLibrary/Infrastructure/Network/Smtp/EmailService.cs b/ServerLibrary/Infrastructure/Network/Smtp/EmailService.cs index 253ad450..b4c5fa3a 100644 --- a/ServerLibrary/Infrastructure/Network/Smtp/EmailService.cs +++ b/ServerLibrary/Infrastructure/Network/Smtp/EmailService.cs @@ -59,8 +59,8 @@ public static void SendActivationEmail(AccountInfo account) } catch (Exception ex) { - SEnvir.Log(ex.Message); - SEnvir.Log(ex.StackTrace); + SEnvir.ServerLogger.Log(ex.Message); + SEnvir.ServerLogger.Log(ex.StackTrace); } }); } @@ -107,8 +107,8 @@ public static void ResendActivationEmail(AccountInfo account) } catch (Exception ex) { - SEnvir.Log(ex.Message); - SEnvir.Log(ex.StackTrace); + SEnvir.ServerLogger.Log(ex.Message); + SEnvir.ServerLogger.Log(ex.StackTrace); } }); } @@ -151,8 +151,8 @@ public static void SendChangePasswordEmail(AccountInfo account, string ipAddress } catch (Exception ex) { - SEnvir.Log(ex.Message); - SEnvir.Log(ex.StackTrace); + SEnvir.ServerLogger.Log(ex.Message); + SEnvir.ServerLogger.Log(ex.StackTrace); } }); } @@ -197,8 +197,8 @@ public static void SendResetPasswordRequestEmail(AccountInfo account, string ipA } catch (Exception ex) { - SEnvir.Log(ex.Message); - SEnvir.Log(ex.StackTrace); + SEnvir.ServerLogger.Log(ex.Message); + SEnvir.ServerLogger.Log(ex.StackTrace); } }); } @@ -239,8 +239,8 @@ public static void SendResetPasswordEmail(AccountInfo account, string password) } catch (Exception ex) { - SEnvir.Log(ex.Message); - SEnvir.Log(ex.StackTrace); + SEnvir.ServerLogger.Log(ex.Message); + SEnvir.ServerLogger.Log(ex.StackTrace); } }); } diff --git a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs index 2979c53e..2df3a691 100644 --- a/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs +++ b/ServerLibrary/Infrastructure/Network/Tcp/ListenerHandler/UserConnectionListenerHandler.cs @@ -22,7 +22,7 @@ public void OnAcceptEnd(TcpListener listener, IAsyncResult result) public void OnAcceptException(Exception ex) { - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); } public void OnTermination() diff --git a/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs b/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs index 6f23ee69..88d2cf29 100644 --- a/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs +++ b/ServerLibrary/Infrastructure/Network/Tcp/TcpServer.cs @@ -12,7 +12,7 @@ public class TcpServer(IListenerHandler ListenerHandler, string IpAddress, ushor private TcpListener Listener; - public void Start(bool log = true) + public void Start() { try { @@ -24,11 +24,11 @@ public void Start(bool log = true) catch (Exception ex) { Started = false; - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); } } - public void Stop(bool log = true) + public void Stop() { TcpListener expiredListener = Listener; diff --git a/ServerLibrary/Infrastructure/Scheduler/RecurringAction.cs b/ServerLibrary/Infrastructure/Scheduler/RecurringAction.cs new file mode 100644 index 00000000..7c9661a6 --- /dev/null +++ b/ServerLibrary/Infrastructure/Scheduler/RecurringAction.cs @@ -0,0 +1,6 @@ +using System; + +namespace Server.Infrastructure.Scheduler +{ + internal record RecurringAction(Action Action, TimeSpan Interval) { } +} diff --git a/ServerLibrary/Infrastructure/Scheduler/SingleThreadScheduler.cs b/ServerLibrary/Infrastructure/Scheduler/SingleThreadScheduler.cs new file mode 100644 index 00000000..5778fb3f --- /dev/null +++ b/ServerLibrary/Infrastructure/Scheduler/SingleThreadScheduler.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Server.Infrastructure.Scheduler +{ + public class SingleThreadScheduler + { + private readonly Thread Thread; + + private readonly object Lock = new(); + private readonly PriorityQueue ScheduledActions = new(); + + public SingleThreadScheduler(string schedulerName = "default") + { + Thread = new Thread(Loop) + { + IsBackground = true, + Name = $"SingleThreadScheduler[{schedulerName}-{Guid.NewGuid()}]" + }; + Thread.Start(); + } + + public void ScheduleRecurring(Action action, TimeSpan interval) + { + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(interval, TimeSpan.Zero); + lock (Lock) + { + ScheduledActions.Enqueue(new RecurringAction(action, interval), DateTime.UtcNow); + Monitor.Pulse(Lock); + } + } + + private void Loop() + { + while (true) //TODO: should i bother to add state (Running, Start, Stop)? + { + foreach (var action in GetAvailableActions()) + { + try { action(); } + catch (Exception) { } + } + lock (Lock) Monitor.Wait(Lock, NextScheduledTime()); + } + } + + private List GetAvailableActions() + { + List actionsDue = []; + var now = DateTime.UtcNow; + + lock (Lock) + { + while (ScheduledActions.TryPeek(out var action, out var nextTime) && nextTime <= now) + { + var awd = ScheduledActions.Dequeue(); + actionsDue.Add(awd.Action); + ScheduledActions.Enqueue(awd, now.Add(awd.Interval)); + } + return actionsDue; + } + } + + private TimeSpan NextScheduledTime() + { + if (ScheduledActions.TryPeek(out var _, out var nextTime)) + { + var nextSchedule = nextTime - DateTime.UtcNow; + if (nextSchedule < TimeSpan.Zero) return TimeSpan.Zero; + return nextSchedule; + } + return TimeSpan.FromMilliseconds(int.MaxValue); + } + } +} diff --git a/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs b/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs index c875a372..ceb7cd99 100644 --- a/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs +++ b/ServerLibrary/Infrastructure/Service/Connection/AbstractConnectionService.cs @@ -64,7 +64,7 @@ internal void Process() { connection.TryDisconnect(); IpManager.Timeout(connection, Config.PacketBanTime); - SEnvir.Log($"{connection.IPAddress} Disconnected, Large Packet"); + SEnvir.ServerLogger.Log($"{connection.IPAddress} Disconnected, Large Packet"); return; } @@ -72,7 +72,7 @@ internal void Process() { connection.TryDisconnect(); IpManager.Timeout(connection, Config.PacketBanTime); - SEnvir.Log($"{connection.IPAddress} Disconnected, Large amount of Packets"); + SEnvir.ServerLogger.Log($"{connection.IPAddress} Disconnected, Large amount of Packets"); return; } @@ -124,7 +124,7 @@ internal void Reset() } catch (Exception ex) { - SEnvir.Log(ex.ToString()); + SEnvir.ServerLogger.Log(ex.ToString()); } IsResetting = false; diff --git a/ServerLibrary/Infrastructure/Service/Connection/UserConnection.cs b/ServerLibrary/Infrastructure/Service/Connection/UserConnection.cs index 9cd8d636..71b7c835 100644 --- a/ServerLibrary/Infrastructure/Service/Connection/UserConnection.cs +++ b/ServerLibrary/Infrastructure/Service/Connection/UserConnection.cs @@ -51,13 +51,13 @@ public UserConnection(TcpClient client, Action disconnectCallbac OnException += (o, e) => { - SEnvir.Log(string.Format("Crashed: Account: {0}, Character: {1}.", Account?.EMailAddress, Player?.Name)); - SEnvir.Log(e.ToString()); - SEnvir.Log(e.StackTrace.ToString()); + SEnvir.ServerLogger.Log(string.Format("Crashed: Account: {0}, Character: {1}.", Account?.EMailAddress, Player?.Name)); + SEnvir.ServerLogger.Log(e.ToString()); + SEnvir.ServerLogger.Log(e.StackTrace.ToString()); File.AppendAllText(@".\Errors.txt", e.StackTrace + Environment.NewLine); }; - SEnvir.Log(string.Format("[Connection] IP Address:{0}", IPAddress)); + SEnvir.ServerLogger.Log(string.Format("[Connection] IP Address:{0}", IPAddress)); UpdateTimeOut(); BeginReceive(); diff --git a/ServerLibrary/Models/Map.cs b/ServerLibrary/Models/Map.cs index ff100554..6f82bc2c 100644 --- a/ServerLibrary/Models/Map.cs +++ b/ServerLibrary/Models/Map.cs @@ -61,7 +61,7 @@ public void Load() if (!File.Exists(path)) { - SEnvir.Log($"Map: {path} not found."); + SEnvir.ServerLogger.Log($"Map: {path} not found."); return; } @@ -107,7 +107,7 @@ private void CreateGuards() { if (info.Monster == null) { - SEnvir.Log($"Failed to spawn Unset Guard Map:{Info.Description}, Location: {info.X}, {info.Y}"); + SEnvir.ServerLogger.Log($"Failed to spawn Unset Guard Map:{Info.Description}, Location: {info.X}, {info.Y}"); continue; } @@ -116,7 +116,7 @@ private void CreateGuards() if (!mob.Spawn(this, new Point(info.X, info.Y))) { - SEnvir.Log($"Failed to spawn Guard Map:{Info.Description}, Location: {info.X}, {info.Y}"); + SEnvir.ServerLogger.Log($"Failed to spawn Guard Map:{Info.Description}, Location: {info.X}, {info.Y}"); continue; } } @@ -134,7 +134,7 @@ private void CreateCastleFlags() if (!mob.Spawn(castle, info)) { - SEnvir.Log($"Failed to spawn Flag Map:{Info.Description}, Location: {info.X}, {info.Y}"); + SEnvir.ServerLogger.Log($"Failed to spawn Flag Map:{Info.Description}, Location: {info.X}, {info.Y}"); continue; } } @@ -197,7 +197,7 @@ public void CreateCellRegions() if (source == null) { - SEnvir.Log($"[Cell] Bad Point, Source: {Info.FileName} {region.Description}, X:{sPoint.X}, Y:{sPoint.Y}"); + SEnvir.ServerLogger.Log($"[Cell] Bad Point, Source: {Info.FileName} {region.Description}, X:{sPoint.X}, Y:{sPoint.Y}"); continue; } diff --git a/ServerLibrary/Models/Monsters/CastleGate.cs b/ServerLibrary/Models/Monsters/CastleGate.cs index 0aafd55d..0291c205 100644 --- a/ServerLibrary/Models/Monsters/CastleGate.cs +++ b/ServerLibrary/Models/Monsters/CastleGate.cs @@ -125,7 +125,7 @@ private void SpawnBlockers() if (!b.Spawn(this.CurrentMap, new Point(this.CurrentLocation.X + block.X, this.CurrentLocation.Y + block.Y))) { - SEnvir.Log(string.Format("{3} blocking mob not spawned at {0} {1}:{2}", CurrentMap.Info.FileName, block.X, block.Y, MonsterInfo.MonsterName)); + SEnvir.ServerLogger.Log(string.Format("{3} blocking mob not spawned at {0} {1}:{2}", CurrentMap.Info.FileName, block.X, block.Y, MonsterInfo.MonsterName)); } } } diff --git a/ServerLibrary/Models/Monsters/Companion.cs b/ServerLibrary/Models/Monsters/Companion.cs index 23e73f28..74dfc9c9 100644 --- a/ServerLibrary/Models/Monsters/Companion.cs +++ b/ServerLibrary/Models/Monsters/Companion.cs @@ -52,7 +52,7 @@ public Companion(UserCompanion companion) if (item.Slot - Globals.EquipmentOffSet >= Equipment.Length) { - SEnvir.Log($"[Bag Companion Equipment] Slot: {item.Slot}, Character: {UserCompanion.Character.CharacterName}, Companion: {UserCompanion.Name}"); + SEnvir.ServerLogger.Log($"[Bag Companion Equipment] Slot: {item.Slot}, Character: {UserCompanion.Character.CharacterName}, Companion: {UserCompanion.Name}"); continue; } @@ -76,7 +76,7 @@ public Companion(UserCompanion companion) if (item.Slot >= Inventory.Length) { - SEnvir.Log($"[Bag Companion Inventory] Slot: {item.Slot}, Character: {UserCompanion.Character.CharacterName}, Companion: {UserCompanion.Name}"); + SEnvir.ServerLogger.Log($"[Bag Companion Inventory] Slot: {item.Slot}, Character: {UserCompanion.Character.CharacterName}, Companion: {UserCompanion.Name}"); continue; } diff --git a/ServerLibrary/Models/PlayerObject.cs b/ServerLibrary/Models/PlayerObject.cs index f2df1762..22afa044 100644 --- a/ServerLibrary/Models/PlayerObject.cs +++ b/ServerLibrary/Models/PlayerObject.cs @@ -875,7 +875,7 @@ public void StartGame() { if (!SetBindPoint()) { - SEnvir.Log($"[Failed to spawn Character] Index: {Character.Index}, Name: {Character.CharacterName}, Failed to reset bind point."); + SEnvir.ServerLogger.Log($"[Failed to spawn Character] Index: {Character.Index}, Name: {Character.CharacterName}, Failed to reset bind point."); Enqueue(new S.StartGame { Result = StartGameResult.UnableToSpawn }); Connection = null; Character = null; @@ -904,7 +904,7 @@ public void StartGame() } else { - SEnvir.Log($"[Failed to spawn Character] Index: {Character.Index}, Name: {Character.CharacterName}"); + SEnvir.ServerLogger.Log($"[Failed to spawn Character] Index: {Character.Index}, Name: {Character.CharacterName}"); Enqueue(new S.StartGame { Result = StartGameResult.UnableToSpawn }); Connection = null; Character = null; @@ -913,7 +913,7 @@ public void StartGame() } else if (!Spawn(Character.CurrentMap, null, 0, CurrentLocation) && !Spawn(Character.BindPoint.BindRegion, null, 0)) { - SEnvir.Log($"[Failed to spawn Character] Index: {Character.Index}, Name: {Character.CharacterName}"); + SEnvir.ServerLogger.Log($"[Failed to spawn Character] Index: {Character.Index}, Name: {Character.CharacterName}"); Enqueue(new S.StartGame { Result = StartGameResult.UnableToSpawn }); Connection = null; Character = null; @@ -1456,7 +1456,7 @@ public void RemoveMount() public void Chat(string text) { if (string.IsNullOrEmpty(text)) return; - SEnvir.LogChat($"{Name}: {text}"); + SEnvir.UserChatLogger.Log($"{Name}: {text}"); //Item Links @@ -1667,7 +1667,7 @@ public void ObserverChat(UserConnection con, string text) con.ReceiveChat(con.Language.ObserverNotLoggedIn, MessageType.System); return; } - SEnvir.LogChat($"{con.Account.LastCharacter.CharacterName}: {text}"); + SEnvir.UserChatLogger.Log($"{con.Account.LastCharacter.CharacterName}: {text}"); string[] parts; @@ -8265,7 +8265,7 @@ public void NameChange(string newName) result.Link.Count = 0; } - SEnvir.Log($"[NAME CHANGED] Old: {Name}, New: {newName}.", true); + SEnvir.ServerLogger.Log($"[NAME CHANGED] Old: {Name}, New: {newName}."); Name = newName; SendChangeUpdate(); @@ -8311,7 +8311,7 @@ public void CaptionChange(string newCaption) } Character.Caption = newCaption; Caption = newCaption; - SEnvir.Log($"[CAPTION CHANGED] {Character.CharacterName} caption changed to: {Caption}", true); + SEnvir.ServerLogger.Log($"[CAPTION CHANGED] {Character.CharacterName} caption changed to: {Caption}"); Connection.ReceiveChat($"Your caption changed to: {Caption}.", MessageType.System); @@ -13286,7 +13286,7 @@ public void Attack(MirDirection direction, MagicType attackMagic) if (attackMagic != validMagic) { - SEnvir.Log($"[ERROR] {Name} requested Attack Skill '{attackMagic}' but valid magic was '{validMagic}'."); + SEnvir.ServerLogger.Log($"[ERROR] {Name} requested Attack Skill '{attackMagic}' but valid magic was '{validMagic}'."); Enqueue(new S.UserLocation { Direction = Direction, Location = CurrentLocation }); return; } diff --git a/ServerLibrary/Models/SpellObject.cs b/ServerLibrary/Models/SpellObject.cs index 22087821..f215463d 100644 --- a/ServerLibrary/Models/SpellObject.cs +++ b/ServerLibrary/Models/SpellObject.cs @@ -97,13 +97,13 @@ public override void Process() if (CurrentCell == null) { - SEnvir.Log($"[ERROR] {Effect} CurrentCell Null."); + SEnvir.ServerLogger.Log($"[ERROR] {Effect} CurrentCell Null."); return; } if (CurrentCell.Objects == null) { - SEnvir.Log($"[ERROR] {Effect} CurrentCell.Objects Null."); + SEnvir.ServerLogger.Log($"[ERROR] {Effect} CurrentCell.Objects Null."); return; } @@ -116,13 +116,13 @@ public override void Process() if (CurrentCell == null) { - SEnvir.Log($"[ERROR] {Effect} CurrentCell Null Loop."); + SEnvir.ServerLogger.Log($"[ERROR] {Effect} CurrentCell Null Loop."); return; } if (CurrentCell.Objects == null) { - SEnvir.Log($"[ERROR] {Effect} CurrentCell.Objects Null Loop."); + SEnvir.ServerLogger.Log($"[ERROR] {Effect} CurrentCell.Objects Null Loop."); return; }