fix: MMO 서버 로직 버그 6건 수정
1. PlayerId 스푸핑 방지: OnTransformPlayer, OnActionPlayer, OnStatePlayer에서 브로드캐스트 전 packet.PlayerId = hashKey로 강제 교체 2. HP/MP 클라이언트 조작 방지: OnStatePlayer에서 범위 클램핑 (0 ≤ Hp ≤ MaxHp, 0 ≤ Mp ≤ MaxMp) 3. CreateParty 파티원 등록 누락 수정: - memberIds 파라미터 사용 시 모든 멤버를 playerPartyMap에 등록 - 리더 중복 추가 방지 (Contains 체크) 4. OnIntoChannel 채널 만석 유령 상태 방지: 이전 채널 제거 후 새 채널 입장 실패 시 이전 채널로 복귀 5. HandleAuth async 경합 방지: authenticatingTokens HashSet으로 동일 토큰 동시 인증 차단 6. 레이드 맵 미반환 수정: TryReleaseRaidMap 헬퍼 추가, OnChangeMap/OnSessionDisconnected에서 레이드 맵(1001+) 유저 0명 시 인스턴스 맵 해제 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,9 @@ public class GameServer : ServerBase
|
||||
private readonly Dictionary<ushort, Action<NetPeer, int, byte[]>> packetHandlers;
|
||||
private readonly UuidGenerator userUuidGenerator;
|
||||
|
||||
// 동일 토큰 동시 인증 방지
|
||||
private readonly HashSet<string> authenticatingTokens = new();
|
||||
|
||||
public GameServer(int port, string connectionString) : base(port, connectionString)
|
||||
{
|
||||
packetHandlers = new Dictionary<ushort, Action<NetPeer, int, byte[]>>
|
||||
@@ -103,6 +106,17 @@ public class GameServer : ServerBase
|
||||
{
|
||||
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
|
||||
string token = accTokenPacket.Token;
|
||||
|
||||
// 동일 토큰 동시 인증 방지
|
||||
if (!authenticatingTokens.Add(token))
|
||||
{
|
||||
Log.Warning("[Server] 동일 토큰 동시 인증 시도 차단 PeerId={Id}", peer.Id);
|
||||
peer.Disconnect();
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string username = "";
|
||||
tokenHash.TryGetValue(token, out int hashKey);
|
||||
if (hashKey <= 1000)
|
||||
@@ -147,6 +161,11 @@ public class GameServer : ServerBase
|
||||
Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id);
|
||||
OnSessionConnected(peer, hashKey);
|
||||
}
|
||||
finally
|
||||
{
|
||||
authenticatingTokens.Remove(token);
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
@@ -28,6 +28,12 @@ public class PartyManager
|
||||
memeberIds = new List<int>();
|
||||
}
|
||||
|
||||
// 리더 중복 방지: 기존 멤버 목록에 리더가 없을 때만 추가
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user