From 66989b22930d34701533e7c95dc43e0411d37c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Fri, 6 Mar 2026 15:48:51 -0300 Subject: [PATCH] refactor: replace Dictionary with ConcurrentDictionary for sessions Replace the Dictionary with ConcurrentDictionary to provide thread-safe access without explicit locking. This eliminates the need for mutex locks around session dictionary operations. Remove mutex locks from OnConnected, OnDisconnected, GetSession, GetSessionByAccountId, GetSessions, and event broadcasting methods since ConcurrentDictionary handles synchronization internally. Update OnDisconnected to use TryRemove with KeyValuePair for atomic reference equality checks, ensuring the correct session is removed during reconnection scenarios (same-channel migration). Remove unused Maple2.Graphics.Interface project. Co-Authored-By: Claude Opus 4.6 --- .../Maple2.Graphics.Interface.csproj | 9 --- Maple2.Server.Game/GameServer.cs | 73 ++++++++----------- 2 files changed, 31 insertions(+), 51 deletions(-) delete mode 100644 Maple2.Graphics.Interface/Maple2.Graphics.Interface.csproj diff --git a/Maple2.Graphics.Interface/Maple2.Graphics.Interface.csproj b/Maple2.Graphics.Interface/Maple2.Graphics.Interface.csproj deleted file mode 100644 index fa71b7ae6..000000000 --- a/Maple2.Graphics.Interface/Maple2.Graphics.Interface.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net8.0 - enable - enable - - - diff --git a/Maple2.Server.Game/GameServer.cs b/Maple2.Server.Game/GameServer.cs index 7d2524626..ed6cfabe8 100644 --- a/Maple2.Server.Game/GameServer.cs +++ b/Maple2.Server.Game/GameServer.cs @@ -21,7 +21,7 @@ public class GameServer : Server { private readonly object mutex = new(); private readonly FieldManager.Factory fieldFactory; private readonly HashSet connectingSessions; - private readonly Dictionary sessions; + private readonly ConcurrentDictionary sessions; private readonly ImmutableList bannerCache; private readonly ConcurrentDictionary premiumMarketCache; private readonly GameStorage gameStorage; @@ -36,7 +36,7 @@ public GameServer(FieldManager.Factory fieldFactory, PacketRouter r _channel = (short) channel; this.fieldFactory = fieldFactory; connectingSessions = []; - sessions = new Dictionary(); + sessions = new ConcurrentDictionary(); this.gameStorage = gameStorage; this.debugGraphicsContext = debugGraphicsContext; this.itemMetadataStorage = itemMetadataStorage; @@ -66,33 +66,34 @@ public GameServer(FieldManager.Factory fieldFactory, PacketRouter r public override void OnConnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - sessions[session.CharacterId] = session; } + sessions[session.CharacterId] = session; } public override void OnDisconnected(GameSession session) { lock (mutex) { connectingSessions.Remove(session); - sessions.Remove(session.CharacterId); } + // Only remove if this is still the registered session (reference equality). + // During same-channel migration, Disconnect() drains the send queue (up to 2s) + // before Dispose runs. In that window the client reconnects and the new session's + // OnConnected replaces this dict entry. Without this check, the old session's + // OnDisconnected would remove the new session, leaving the player unregistered + // and causing all subsequent heartbeats to fail. + // For cross-channel migration this is a non-issue since each server has its own dict. + sessions.TryRemove(KeyValuePair.Create(session.CharacterId, session)); } public bool GetSession(long characterId, [NotNullWhen(true)] out GameSession? session) { - lock (mutex) { - return sessions.TryGetValue(characterId, out session); - } + return sessions.TryGetValue(characterId, out session); } public GameSession? GetSessionByAccountId(long accountId) { - lock (mutex) { - return sessions.Values.FirstOrDefault(session => session.AccountId == accountId); - } + return sessions.Values.FirstOrDefault(session => session.AccountId == accountId); } public List GetSessions() { - lock (mutex) { - return sessions.Values.ToList(); - } + return sessions.Values.ToList(); } protected override void AddSession(GameSession session) { @@ -129,10 +130,8 @@ public void AddEvent(GameEvent gameEvent) { return; } - lock (mutex) { - foreach (GameSession session in sessions.Values) { - session.Send(GameEventPacket.Add(gameEvent)); - } + foreach (GameSession session in sessions.Values) { + session.Send(GameEventPacket.Add(gameEvent)); } } @@ -141,10 +140,8 @@ public void RemoveEvent(int eventId) { return; } - lock (mutex) { - foreach (GameSession session in sessions.Values) { - session.Send(GameEventPacket.Remove(gameEvent.Id)); - } + foreach (GameSession session in sessions.Values) { + session.Send(GameEventPacket.Remove(gameEvent.Id)); } } @@ -169,26 +166,20 @@ public ICollection GetPremiumMarketItems(params int[] tabIds) } public void DailyReset() { - lock (mutex) { - foreach (GameSession session in sessions.Values) { - session.DailyReset(); - } + foreach (GameSession session in sessions.Values) { + session.DailyReset(); } } public void WeeklyReset() { - lock (mutex) { - foreach (GameSession session in sessions.Values) { - session.WeeklyReset(); - } + foreach (GameSession session in sessions.Values) { + session.WeeklyReset(); } } public void MonthlyReset() { - lock (mutex) { - foreach (GameSession session in sessions.Values) { - session.MonthlyReset(); - } + foreach (GameSession session in sessions.Values) { + session.MonthlyReset(); } } @@ -200,21 +191,19 @@ public override Task StopAsync(CancellationToken cancellationToken) { session.Send(NoticePacket.Disconnect(new InterfaceText("GameServer Maintenance"))); session.Dispose(); } - foreach (GameSession session in sessions.Values) { - session.Send(NoticePacket.Disconnect(new InterfaceText("GameServer Maintenance"))); - session.Dispose(); - } - fieldFactory.Dispose(); } + foreach (GameSession session in sessions.Values) { + session.Send(NoticePacket.Disconnect(new InterfaceText("GameServer Maintenance"))); + session.Dispose(); + } + fieldFactory.Dispose(); return base.StopAsync(cancellationToken); } public void Broadcast(ByteWriter packet) { - lock (mutex) { - foreach (GameSession session in sessions.Values) { - session.Send(packet); - } + foreach (GameSession session in sessions.Values) { + session.Send(packet); } }