Skip to content

Commit 819ff6f

Browse files
refactor: replace Dictionary with ConcurrentDictionary for sessions (#663)
Replace the Dictionary<long, GameSession> with ConcurrentDictionary<long, GameSession> 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 <noreply@anthropic.com>
1 parent 6590f86 commit 819ff6f

2 files changed

Lines changed: 31 additions & 51 deletions

File tree

Maple2.Graphics.Interface/Maple2.Graphics.Interface.csproj

Lines changed: 0 additions & 9 deletions
This file was deleted.

Maple2.Server.Game/GameServer.cs

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public class GameServer : Server<GameSession> {
2121
private readonly object mutex = new();
2222
private readonly FieldManager.Factory fieldFactory;
2323
private readonly HashSet<GameSession> connectingSessions;
24-
private readonly Dictionary<long, GameSession> sessions;
24+
private readonly ConcurrentDictionary<long, GameSession> sessions;
2525
private readonly ImmutableList<SystemBanner> bannerCache;
2626
private readonly ConcurrentDictionary<int, PremiumMarketItem> premiumMarketCache;
2727
private readonly GameStorage gameStorage;
@@ -36,7 +36,7 @@ public GameServer(FieldManager.Factory fieldFactory, PacketRouter<GameSession> r
3636
_channel = (short) channel;
3737
this.fieldFactory = fieldFactory;
3838
connectingSessions = [];
39-
sessions = new Dictionary<long, GameSession>();
39+
sessions = new ConcurrentDictionary<long, GameSession>();
4040
this.gameStorage = gameStorage;
4141
this.debugGraphicsContext = debugGraphicsContext;
4242
this.itemMetadataStorage = itemMetadataStorage;
@@ -66,33 +66,34 @@ public GameServer(FieldManager.Factory fieldFactory, PacketRouter<GameSession> r
6666
public override void OnConnected(GameSession session) {
6767
lock (mutex) {
6868
connectingSessions.Remove(session);
69-
sessions[session.CharacterId] = session;
7069
}
70+
sessions[session.CharacterId] = session;
7171
}
7272

7373
public override void OnDisconnected(GameSession session) {
7474
lock (mutex) {
7575
connectingSessions.Remove(session);
76-
sessions.Remove(session.CharacterId);
7776
}
77+
// Only remove if this is still the registered session (reference equality).
78+
// During same-channel migration, Disconnect() drains the send queue (up to 2s)
79+
// before Dispose runs. In that window the client reconnects and the new session's
80+
// OnConnected replaces this dict entry. Without this check, the old session's
81+
// OnDisconnected would remove the new session, leaving the player unregistered
82+
// and causing all subsequent heartbeats to fail.
83+
// For cross-channel migration this is a non-issue since each server has its own dict.
84+
sessions.TryRemove(KeyValuePair.Create(session.CharacterId, session));
7885
}
7986

8087
public bool GetSession(long characterId, [NotNullWhen(true)] out GameSession? session) {
81-
lock (mutex) {
82-
return sessions.TryGetValue(characterId, out session);
83-
}
88+
return sessions.TryGetValue(characterId, out session);
8489
}
8590

8691
public GameSession? GetSessionByAccountId(long accountId) {
87-
lock (mutex) {
88-
return sessions.Values.FirstOrDefault(session => session.AccountId == accountId);
89-
}
92+
return sessions.Values.FirstOrDefault(session => session.AccountId == accountId);
9093
}
9194

9295
public List<GameSession> GetSessions() {
93-
lock (mutex) {
94-
return sessions.Values.ToList();
95-
}
96+
return sessions.Values.ToList();
9697
}
9798

9899
protected override void AddSession(GameSession session) {
@@ -129,10 +130,8 @@ public void AddEvent(GameEvent gameEvent) {
129130
return;
130131
}
131132

132-
lock (mutex) {
133-
foreach (GameSession session in sessions.Values) {
134-
session.Send(GameEventPacket.Add(gameEvent));
135-
}
133+
foreach (GameSession session in sessions.Values) {
134+
session.Send(GameEventPacket.Add(gameEvent));
136135
}
137136
}
138137

@@ -141,10 +140,8 @@ public void RemoveEvent(int eventId) {
141140
return;
142141
}
143142

144-
lock (mutex) {
145-
foreach (GameSession session in sessions.Values) {
146-
session.Send(GameEventPacket.Remove(gameEvent.Id));
147-
}
143+
foreach (GameSession session in sessions.Values) {
144+
session.Send(GameEventPacket.Remove(gameEvent.Id));
148145
}
149146
}
150147

@@ -169,26 +166,20 @@ public ICollection<PremiumMarketItem> GetPremiumMarketItems(params int[] tabIds)
169166
}
170167

171168
public void DailyReset() {
172-
lock (mutex) {
173-
foreach (GameSession session in sessions.Values) {
174-
session.DailyReset();
175-
}
169+
foreach (GameSession session in sessions.Values) {
170+
session.DailyReset();
176171
}
177172
}
178173

179174
public void WeeklyReset() {
180-
lock (mutex) {
181-
foreach (GameSession session in sessions.Values) {
182-
session.WeeklyReset();
183-
}
175+
foreach (GameSession session in sessions.Values) {
176+
session.WeeklyReset();
184177
}
185178
}
186179

187180
public void MonthlyReset() {
188-
lock (mutex) {
189-
foreach (GameSession session in sessions.Values) {
190-
session.MonthlyReset();
191-
}
181+
foreach (GameSession session in sessions.Values) {
182+
session.MonthlyReset();
192183
}
193184
}
194185

@@ -200,21 +191,19 @@ public override Task StopAsync(CancellationToken cancellationToken) {
200191
session.Send(NoticePacket.Disconnect(new InterfaceText("GameServer Maintenance")));
201192
session.Dispose();
202193
}
203-
foreach (GameSession session in sessions.Values) {
204-
session.Send(NoticePacket.Disconnect(new InterfaceText("GameServer Maintenance")));
205-
session.Dispose();
206-
}
207-
fieldFactory.Dispose();
208194
}
195+
foreach (GameSession session in sessions.Values) {
196+
session.Send(NoticePacket.Disconnect(new InterfaceText("GameServer Maintenance")));
197+
session.Dispose();
198+
}
199+
fieldFactory.Dispose();
209200

210201
return base.StopAsync(cancellationToken);
211202
}
212203

213204
public void Broadcast(ByteWriter packet) {
214-
lock (mutex) {
215-
foreach (GameSession session in sessions.Values) {
216-
session.Send(packet);
217-
}
205+
foreach (GameSession session in sessions.Values) {
206+
session.Send(packet);
218207
}
219208
}
220209

0 commit comments

Comments
 (0)