diff --git a/MMOTestServer/MMOserver/Game/GameServer.cs b/MMOTestServer/MMOserver/Game/GameServer.cs index 4318702..4ae78d1 100644 --- a/MMOTestServer/MMOserver/Game/GameServer.cs +++ b/MMOTestServer/MMOserver/Game/GameServer.cs @@ -18,6 +18,9 @@ public class GameServer : ServerBase private readonly Dictionary> packetHandlers; private readonly UuidGenerator userUuidGenerator; + // 동일 토큰 동시 인증 방지 + private readonly HashSet authenticatingTokens = new(); + public GameServer(int port, string connectionString) : base(port, connectionString) { packetHandlers = new Dictionary> @@ -103,49 +106,65 @@ public class GameServer : ServerBase { AccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); string token = accTokenPacket.Token; - string username = ""; - tokenHash.TryGetValue(token, out int hashKey); - if (hashKey <= 1000) + + // 동일 토큰 동시 인증 방지 + if (!authenticatingTokens.Add(token)) { - hashKey = userUuidGenerator.Create(); + Log.Warning("[Server] 동일 토큰 동시 인증 시도 차단 PeerId={Id}", peer.Id); + peer.Disconnect(); + return; } - if (sessions.TryGetValue(hashKey, out NetPeer? existing)) + try { - // WiFi → LTE 전환 등 재연결: 이전 피어 교체 (토큰 재검증 불필요) - existing.Tag = null; - sessions.Remove(hashKey); - Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id); - existing.Disconnect(); - } - else - { - // 신규 연결: 웹서버에 JWT 검증 요청 - username = await RestApi.Instance.VerifyTokenAsync(token); - if (username == null) + string username = ""; + tokenHash.TryGetValue(token, out int hashKey); + if (hashKey <= 1000) { - Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id); - userUuidGenerator.Release(hashKey); - peer.Disconnect(); - return; + hashKey = userUuidGenerator.Create(); } - Log.Information("[Server] 토큰 검증 성공 Username={Username} PeerId={Id}", username, peer.Id); - } + if (sessions.TryGetValue(hashKey, out NetPeer? existing)) + { + // WiFi → LTE 전환 등 재연결: 이전 피어 교체 (토큰 재검증 불필요) + existing.Tag = null; + sessions.Remove(hashKey); + Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id); + existing.Disconnect(); + } + else + { + // 신규 연결: 웹서버에 JWT 검증 요청 + username = await RestApi.Instance.VerifyTokenAsync(token); + if (username == null) + { + Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id); + userUuidGenerator.Release(hashKey); + peer.Disconnect(); + return; + } - peer.Tag = new Session(hashKey, peer); - ((Session)peer.Tag).Token = token; - if (username.Length > 0) + Log.Information("[Server] 토큰 검증 성공 Username={Username} PeerId={Id}", username, peer.Id); + } + + peer.Tag = new Session(hashKey, peer); + ((Session)peer.Tag).Token = token; + if (username.Length > 0) + { + ((Session)peer.Tag).UserName = username; + } + + sessions[hashKey] = peer; + tokenHash[token] = hashKey; + pendingPeers.Remove(peer.Id); + + Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); + OnSessionConnected(peer, hashKey); + } + finally { - ((Session)peer.Tag).UserName = username; + authenticatingTokens.Remove(token); } - - sessions[hashKey] = peer; - tokenHash[token] = hashKey; - pendingPeers.Remove(peer.Id); - - Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); - OnSessionConnected(peer, hashKey); } protected override void OnSessionConnected(NetPeer peer, int hashKey) @@ -186,6 +205,9 @@ public class GameServer : ServerBase // 파티 자동 탈퇴 HandlePartyLeaveOnExit(channelId, hashKey); + // 레이드 맵이었으면 해제 체크 + TryReleaseRaidMap(cm.GetChannel(channelId), player.CurrentMapId); + // 같은 채널 유저들에게 나갔다고 알림 SendExitChannelPacket(peer, hashKey, channelId, player); } @@ -447,6 +469,20 @@ public class GameServer : ServerBase { Log.Warning("[GameServer] INTO_CHANNEL 채널 인원 초과 HashKey={Key} ChannelId={ChannelId} UserCount={Count}/{Max}", hashKey, packet.ChannelId, newChannel.UserCount, newChannel.UserCountMax); + + // 이전 채널에서 이미 제거된 경우 → 이전 채널로 복귀 + if (preChannelId >= 0) + { + Player? fallbackPlayer = new() + { + HashKey = hashKey, + PlayerId = hashKey, + Nickname = ((Session)peer.Tag).UserName + }; + cm.AddUser(preChannelId, hashKey, fallbackPlayer, peer); + Log.Information("[GameServer] INTO_CHANNEL 만석 → 이전 채널({ChannelId})로 복귀 HashKey={Key}", preChannelId, hashKey); + } + byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, new IntoChannelPacket { ChannelId = -1 }); SendTo(peer, full); @@ -662,6 +698,9 @@ public class GameServer : ServerBase player.PosZ = packet.Position.Z; player.RotY = packet.RotY; + // PlayerId 강제 교체 (클라이언트 스푸핑 방지) + packet.PlayerId = hashKey; + // 같은 맵 유저들에게 위치/방향 브로드캐스트 (나 제외) byte[] data = PacketSerializer.Serialize((ushort)PacketCode.TRANSFORM_PLAYER, packet); BroadcastToMap(channelId, player.CurrentMapId, data, peer, DeliveryMethod.Unreliable); @@ -684,6 +723,9 @@ public class GameServer : ServerBase return; } + // PlayerId 강제 교체 (클라이언트 스푸핑 방지) + packet.PlayerId = hashKey; + // 같은 맵 유저들에게 행동 브로드캐스트 (나 제외) byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ACTION_PLAYER, packet); BroadcastToMap(channelId, player.CurrentMapId, data, peer); @@ -707,6 +749,15 @@ public class GameServer : ServerBase return; } + // PlayerId 강제 교체 (클라이언트 스푸핑 방지) + packet.PlayerId = hashKey; + + // HP/MP 범위 클램핑 (클라이언트 조작 방지) + if (packet.MaxHp < 0) packet.MaxHp = 0; + if (packet.MaxMp < 0) packet.MaxMp = 0; + packet.Hp = Math.Clamp(packet.Hp, 0, packet.MaxHp); + packet.Mp = Math.Clamp(packet.Mp, 0, packet.MaxMp); + player.Hp = packet.Hp; player.MaxHp = packet.MaxHp; player.Mp = packet.Mp; @@ -957,6 +1008,10 @@ public class GameServer : ServerBase } SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response)); + + // 레이드 맵(1001+)에서 나갔고 남은 유저가 0이면 인스턴스 맵 해제 + TryReleaseRaidMap(channel, oldMapId); + Log.Debug("[GameServer] CHANGE_MAP HashKey={Key} OldMap={OldMapId} NewMap={MapId}", hashKey, oldMapId, packet.MapId); } @@ -1188,6 +1243,22 @@ public class GameServer : ServerBase BroadcastToChannel(channelId, data); } + // 레이드 맵(1001+)에서 유저가 빠졌을 때, 남은 유저가 0이면 인스턴스 맵 해제 + private static void TryReleaseRaidMap(Channel.Channel channel, int mapId) + { + if (mapId < 1001) + { + return; + } + + AMap? map = channel.GetMap(mapId); + if (map != null && map.GetUsers().Count == 0) + { + channel.RemoveInstanceMap(mapId); + Log.Debug("[GameServer] 레이드 맵 해제 MapId={MapId}", mapId); + } + } + private void SendError(NetPeer peer, ErrorCode code) { ErrorPacket err = new() { Code = code }; diff --git a/MMOTestServer/MMOserver/Game/Party/PartyManager.cs b/MMOTestServer/MMOserver/Game/Party/PartyManager.cs index 7dca0b9..c87b642 100644 --- a/MMOTestServer/MMOserver/Game/Party/PartyManager.cs +++ b/MMOTestServer/MMOserver/Game/Party/PartyManager.cs @@ -28,6 +28,12 @@ public class PartyManager memeberIds = new List(); } + // 리더 중복 방지: 기존 멤버 목록에 리더가 없을 때만 추가 + if (!memeberIds.Contains(leaderId)) + { + memeberIds.Add(leaderId); + } + party = new PartyInfo { PartyId = partyId, @@ -35,10 +41,14 @@ public class PartyManager PartyName = partyName, PartyMemberIds = memeberIds }; - party.PartyMemberIds.Add(leaderId); parties[partyId] = party; - playerPartyMap[leaderId] = partyId; + + // 모든 멤버를 playerPartyMap에 등록 + foreach (int memberId in memeberIds) + { + playerPartyMap[memberId] = partyId; + } return true; }