@@ -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