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);
}
}