using LiteNetLib; using LiteNetLib.Utils; using MMOserver.Api; using MMOserver.Game.Channel; using MMOserver.Game.Channel.Maps; using MMOserver.Game.Party; using MMOserver.Packet; using MMOserver.Utils; using ProtoBuf; using Serilog; using ServerLib.Packet; using ServerLib.Service; namespace MMOserver.Game; public class GameServer : ServerBase { private readonly Dictionary> packetHandlers; private readonly UuidGenerator userUuidGenerator; public GameServer(int port, string connectionString) : base(port, connectionString) { packetHandlers = new Dictionary> { [(ushort)PacketCode.INTO_CHANNEL] = OnIntoChannel, [(ushort)PacketCode.INTO_CHANNEL_PARTY] = OnIntoChannelParty, [(ushort)PacketCode.EXIT_CHANNEL] = OnExitChannel, [(ushort)PacketCode.TRANSFORM_PLAYER] = OnTransformPlayer, [(ushort)PacketCode.ACTION_PLAYER] = OnActionPlayer, [(ushort)PacketCode.STATE_PLAYER] = OnStatePlayer, [(ushort)PacketCode.CHANGE_MAP] = OnChangeMap, [(ushort)PacketCode.PARTY_CHANGE_MAP] = OnPartyChangeMap, [(ushort)PacketCode.INTO_BOSS_RAID] = OnIntoBossRaid, [(ushort)PacketCode.REQUEST_PARTY] = OnRequestParty, [(ushort)PacketCode.CHAT] = OnChat }; userUuidGenerator = new UuidGenerator(); } protected override void HandleEcho(NetPeer peer, byte[] payload) { if (payload.Length < 4) { Log.Warning("[Server] Echo 페이로드 크기 오류 PeerId={Id} Length={Len}", peer.Id, payload.Length); return; } // 세션에 넣지는 않는다. NetDataReader reader = new(payload); short code = reader.GetShort(); short bodyLength = reader.GetShort(); EchoPacket echoPacket = Serializer.Deserialize(payload.AsMemory(4)); Log.Debug("[Echo] : addr={Addr}, str={Str}", peer.Address, echoPacket.Str); // Echo메시지는 순서보장 안함 HOL Blocking 제거 SendTo(peer, payload, DeliveryMethod.ReliableUnordered); } protected override void HandleAuthDummy(NetPeer peer, byte[] payload) { DummyAccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); int hashKey = accTokenPacket.Token; 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(); } peer.Tag = new Session(hashKey, peer); sessions[hashKey] = peer; pendingPeers.Remove(peer.Id); if (hashKey <= 1000) { // 더미 클라다. ChannelManager cm = ChannelManager.Instance; Player newPlayer = new() { HashKey = hashKey, PlayerId = hashKey, Nickname = hashKey.ToString(), CurrentMapId = 1 }; cm.AddUser(1, hashKey, newPlayer, peer); } else { Log.Error("[Server] Dummy 클라이언트가 아닙니다. 연결을 종료합니다. HashKey={Key} PeerId={Id}", hashKey, peer.Id); peer.Disconnect(); return; } Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); OnSessionConnected(peer, hashKey); } protected override async void HandleAuth(NetPeer peer, byte[] payload) { AccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); string token = accTokenPacket.Token; string? verifiedUsername = null; tokenHash.TryGetValue(token, out int hashKey); if (hashKey <= 1000) { hashKey = userUuidGenerator.Create(); } 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 검증 요청 string? username = await RestApi.Instance.VerifyTokenAsync(token); if (username == null || username.Trim().Length <= 0) { Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id); userUuidGenerator.Release(hashKey); peer.Disconnect(); return; } Log.Information("[Server] 토큰 검증 성공 Username={Username} PeerId={Id}", username, peer.Id); verifiedUsername = username; } peer.Tag = new Session(hashKey, peer); ((Session)peer.Tag).Token = token; if (verifiedUsername != null) { ((Session)peer.Tag).Username = verifiedUsername; ((Session)peer.Tag).UserName = verifiedUsername; } 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) { Log.Information("[GameServer] 세션 연결 HashKey={Key} PeerId={Id}", hashKey, peer.Id); // 만약 wifi-lte 로 바꿔졌다 이때 이미 로비에 들어가 있다면 넘긴다. ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId >= 0) { // 재연결: Channel 내 peer 참조 갱신 후 채널 유저 목록 전송 cm.GetChannel(channelId).UpdatePeer(hashKey, peer); SendIntoChannelPacket(peer, hashKey); } else { // 모든 채널 정보 던진다 SendLoadChannelPacket(peer, hashKey); } } protected override void OnSessionDisconnected(NetPeer peer, int hashKey, DisconnectInfo info) { ChannelManager cm = ChannelManager.Instance; // 제거 전에 채널/플레이어 정보 저장 (브로드캐스트에 필요) int channelId = cm.HasUser(hashKey); Player? player = channelId >= 0 ? cm.GetChannel(channelId).GetPlayer(hashKey) : null; if (cm.RemoveUser(hashKey)) { Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason); if (channelId >= 0 && player != null) { // 파티 자동 탈퇴 HandlePartyLeaveOnExit(channelId, hashKey); // 같은 채널 유저들에게 나갔다고 알림 SendExitChannelPacket(peer, hashKey, channelId, player); } } userUuidGenerator.Release(hashKey); } protected override void HandlePacket(NetPeer peer, int hashKey, ushort type, byte[] payload) { if (packetHandlers.TryGetValue(type, out Action? handler)) { try { handler(peer, hashKey, payload); } catch (Exception ex) { Log.Error(ex, "[GameServer] 패킷 처리 중 예외 Type={Type} HashKey={Key}", type, hashKey); } } else { Log.Warning("[GameServer] 알 수 없는 패킷 Type={Type}", type); } } // ============================================================ // 보내는 패킷 // ============================================================ private void SendLoadChannelPacket(NetPeer peer, int hashKey) { LoadChannelPacket loadChannelPacket = new(); foreach (Channel.Channel channel in ChannelManager.Instance.GetChannels().Values) { if (channel.ChannelId <= 0) { continue; } if (channel.ChannelId >= 1000) { continue; } ChannelInfo info = new(); info.ChannelId = channel.ChannelId; info.ChannelUserCount = channel.UserCount; info.ChannelUserMax = channel.UserCountMax; loadChannelPacket.Channels.Add(info); } byte[] data = PacketSerializer.Serialize((ushort)PacketCode.LOAD_CHANNEL, loadChannelPacket); SendTo(peer, data); } // 나간 유저를 같은 채널 유저들에게 알림 (UPDATE_CHANNEL_USER IsAdd=false) private void SendExitChannelPacket(NetPeer peer, int hashKey, int channelId, Player player) { UpdateChannelUserPacket packet = new() { Players = ToPlayerInfo(player), IsAdd = false }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, packet); // 이미 채널에서 제거된 후라 나간 본인에게는 전송되지 않음 BroadcastToChannel(channelId, data); } // 채널 입장 시 패킷 전송 // - 새 유저에게 : 기존 채널 유저 목록 (INTO_CHANNEL) // - 기존 유저들에게 : 새 유저 입장 알림 (UPDATE_CHANNEL_USER IsAdd=true) private void SendIntoChannelPacket(NetPeer peer, int hashKey) { ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } Channel.Channel channel = cm.GetChannel(channelId); Player? myPlayer = channel.GetPlayer(hashKey); // 1. 새 유저에게: 자신을 제외한 기존 채널 유저 목록 + 파티 목록 전송 IntoChannelPacket response = new() { ChannelId = channelId }; foreach (int userId in channel.GetConnectUsers()) { if (userId == hashKey) { continue; } Player? p = channel.GetPlayer(userId); if (p != null) { response.Players.Add(ToPlayerInfo(p)); } } foreach (PartyInfo party in channel.GetPartyManager().GetAllParties()) { response.Parties.Add(new PartyInfoData { PartyId = party.PartyId, LeaderId = party.LeaderId, MemberPlayerIds = new List(party.PartyMemberIds), PartyName = party.PartyName }); } byte[] toNewUser = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, response); SendTo(peer, toNewUser); // 2. 기존 유저들에게: 새 유저 입장 알림 if (myPlayer != null) { UpdateChannelUserPacket notify = new() { Players = ToPlayerInfo(myPlayer), IsAdd = true }; byte[] toOthers = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, notify); BroadcastToChannel(channelId, toOthers, peer); } } // 채널 입장 시 패킷 전송 // - 자신 유저에게 : 내 정보 (LOAD_GAME) private void SendLoadGame(NetPeer peer, int hashKey) { ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); Player? player = channelId >= 0 ? cm.GetChannel(channelId).GetPlayer(hashKey) : null; if (player == null) { Log.Warning("[GameServer] LOAD_GAME 플레이어 없음 HashKey={Key}", hashKey); byte[] denied = PacketSerializer.Serialize((ushort)PacketCode.LOAD_GAME, new LoadGamePacket { IsAccepted = false }); SendTo(peer, denied); return; } LoadGamePacket packet = new() { IsAccepted = true, Player = ToPlayerInfo(player), MaplId = channelId }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.LOAD_GAME, packet); SendTo(peer, data); Log.Debug("[GameServer] LOAD_GAME HashKey={Key} PlayerId={PlayerId} ChannelId={ChannelId}", hashKey, player.PlayerId, channelId); } // ============================================================ // 채널 브로드캐스트 헬퍼 // ============================================================ // 특정 채널의 모든 유저에게 전송 (exclude 지정 시 해당 피어 제외) // Channel이 NetPeer를 직접 보유하므로 sessions 교차 조회 없음 private void BroadcastToChannel(int channelId, byte[] data, NetPeer? exclude = null, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { Channel.Channel channel = ChannelManager.Instance.GetChannel(channelId); foreach (NetPeer targetPeer in channel.GetConnectPeers()) { if (exclude != null && targetPeer.Id == exclude.Id) { continue; } SendTo(targetPeer, data, method); } } // 특정 맵의 유저들에게 전송 (exclude 지정 시 해당 피어 제외) private void BroadcastToMap(int channelId, int mapId, byte[] data, NetPeer? exclude = null, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { AMap? map = ChannelManager.Instance.GetChannel(channelId).GetMap(mapId); if (map == null) { return; } foreach (int userId in map.GetUsers().Keys) { if (!sessions.TryGetValue(userId, out NetPeer? targetPeer)) { continue; } if (exclude != null && targetPeer.Id == exclude.Id) { continue; } SendTo(targetPeer, data, method); } } // ============================================================ // Player ↔ PlayerInfo 변환 (패킷 전송 시에만 사용) // ============================================================ private static PlayerInfo ToPlayerInfo(Player player) { return new PlayerInfo { PlayerId = player.PlayerId, Nickname = player.Nickname, Level = player.Level, Hp = player.Hp, MaxHp = player.MaxHp, Mp = player.Mp, MaxMp = player.MaxMp, Position = new Position { X = player.PosX, Y = player.PosY, Z = player.PosZ }, RotY = player.RotY, Experience = player.Experience, NextExp = player.NextExp, AttackPower = player.AttackPower, AttackRange = player.AttackRange, SprintMultiplier = player.SprintMultiplier, }; } // ============================================================ // 패킷 핸들러 // ============================================================ private async void OnIntoChannel(NetPeer peer, int hashKey, byte[] payload) { IntoChannelPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; // 이전에 다른 채널에 있었는지 체크 int preChannelId = cm.HasUser(hashKey); if (preChannelId >= 0) { // 제거 전에 채널/플레이어 정보 저장 (브로드캐스트에 필요) Player? player = cm.GetChannel(preChannelId).GetPlayer(hashKey); // 파티 자동 탈퇴 HandlePartyLeaveOnExit(preChannelId, hashKey); cm.RemoveUser(hashKey); Log.Debug("[GameServer] EXIT_CHANNEL HashKey={Key} PlayerId={PlayerId}", hashKey, preChannelId); // 같은 채널 유저들에게 나갔다고 알림 if (player != null) { SendExitChannelPacket(peer, hashKey, preChannelId, player); } } Channel.Channel newChannel = cm.GetChannel(packet.ChannelId); // 최대 인원 체크 if (newChannel.UserCount >= newChannel.UserCountMax) { Log.Warning("[GameServer] INTO_CHANNEL 채널 인원 초과 HashKey={Key} ChannelId={ChannelId} UserCount={Count}/{Max}", hashKey, packet.ChannelId, newChannel.UserCount, newChannel.UserCountMax); byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, new IntoChannelPacket { ChannelId = -1 }); SendTo(peer, full); return; } // API 서버에서 플레이어 프로필 로드 Player newPlayer = new Player { HashKey = hashKey, PlayerId = hashKey, Nickname = ((Session)peer.Tag).UserName }; Session? session = peer.Tag as Session; string? username = session?.Username; if (!string.IsNullOrEmpty(username)) { try { RestApi.PlayerProfileResponse? profile = await RestApi.Instance.GetPlayerProfileAsync(username); if (profile != null) { newPlayer.Nickname = string.IsNullOrEmpty(profile.Nickname) ? username : profile.Nickname; newPlayer.Level = profile.Level; newPlayer.MaxHp = (int)profile.MaxHp; newPlayer.Hp = (int)profile.MaxHp; newPlayer.MaxMp = (int)profile.MaxMp; newPlayer.Mp = (int)profile.MaxMp; newPlayer.Experience = profile.Experience; newPlayer.NextExp = profile.NextExp; newPlayer.AttackPower = (float)profile.AttackPower; newPlayer.AttackRange = (float)profile.AttackRange; newPlayer.SprintMultiplier = (float)profile.SprintMultiplier; Log.Information("[GameServer] 프로필 로드 완료 Username={Username} Level={Level} MaxHp={MaxHp}", username, profile.Level, profile.MaxHp); } else { newPlayer.Nickname = username; Log.Warning("[GameServer] 프로필 로드 실패 — 기본값 사용 Username={Username}", username); } } catch (Exception ex) { newPlayer.Nickname = username; Log.Error(ex, "[GameServer] 프로필 로드 예외 Username={Username}", username); } } cm.AddUser(packet.ChannelId, hashKey, newPlayer, peer); Log.Debug("[GameServer] INTO_CHANNEL HashKey={Key} ChannelId={ChannelId}", hashKey, packet.ChannelId); // 접속된 모든 유저 정보 전달 SendIntoChannelPacket(peer, hashKey); // 내 정보 전달 SendLoadGame(peer, hashKey); // 초기 맵(로비 1번) 진입 알림 // Channel.AddUser → ChangeMap(1) 에서 이미 맵에 추가됨 PlayerInfo playerInfo = ToPlayerInfo(newPlayer); int initMapId = newPlayer.CurrentMapId; // 기존 맵 유저들에게 입장 알림 (본인 제외) ChangeMapPacket enterNotify = new() { MapId = initMapId, IsAdd = true, Player = playerInfo }; BroadcastToMap(packet.ChannelId, initMapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, enterNotify), peer); // 본인에게 현재 맵의 플레이어 목록 전달 ChangeMapPacket response = new() { MapId = initMapId }; AMap? initMap = newChannel.GetMap(initMapId); if (initMap != null) { foreach (var (userId, p) in initMap.GetUsers()) { if (userId == hashKey) { continue; } response.Players.Add(ToPlayerInfo(p)); } } SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response)); } private void OnIntoChannelParty(NetPeer peer, int hashKey, byte[] payload) { IntoChannelPartyPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int preChannelId = cm.HasUser(hashKey); Channel.Channel preChannel = cm.GetChannel(preChannelId); Channel.Channel newChannel = cm.GetChannel(packet.ChannelId); PartyInfo? preParty = preChannel.GetPartyManager().GetParty(packet.PartyId); // 이전에 다른 채널에 있었는지 체크 / 파티이동은 이미 접속한 상태여야 한다. if (preChannelId < 0 || preParty == null) { Log.Warning("[GameServer] INTO_CHANNEL_PARTY 해당 파티 없음"); return; } // 새로운 파티를 복사한다 PartyInfo tempParty = new(); tempParty.DeepCopySemi(preParty); // 최대 인원 체크 if (newChannel.UserCount + preParty.PartyMemberIds.Count >= newChannel.UserCountMax) { Log.Warning("[GameServer] INTO_CHANNEL_PARTY 채널 인원 초과 HashKey={Key} ChannelId={ChannelId} UserCount={Count}/{Max}", hashKey, packet.ChannelId, newChannel.UserCount, newChannel.UserCountMax); byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, new IntoChannelPacket { ChannelId = -1 }); SendTo(peer, full); return; } // 기존 채널에서 제거 + 기존 채널 유저들에게 나감 알림 foreach (int memberId in preParty.PartyMemberIds) { Player? player = preChannel.GetPlayer(memberId); if (player != null) { UpdateChannelUserPacket exitNotify = new() { Players = ToPlayerInfo(player), IsAdd = false }; byte[] exitData = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, exitNotify); BroadcastToChannel(preChannelId, exitData); // 이전 채널에서 제거 preChannel.RemoveUser(memberId); // 현재 존재하는 파티원만 추가한다. tempParty.PartyMemberIds.Add(memberId); } } // 이전채널에서 파티를 지운다. preChannel.GetPartyManager().DeleteParty(hashKey, packet.PartyId, out preParty); UpdatePartyPacket notify = new() { PartyId = preParty!.PartyId, Type = PartyUpdateType.DELETE, LeaderId = preParty.LeaderId }; // 채널 전체 파티 목록 갱신 BroadcastToChannel(preChannelId, PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify)); // 새로운 채널에 파티원 넣기 foreach (int memberId in tempParty.PartyMemberIds) { sessions.TryGetValue(memberId, out NetPeer? memberPeer); if (memberPeer != null) { // 새 채널에 유저 추가 Player newPlayer = new() { HashKey = memberId, PlayerId = memberId, Nickname = memberId.ToString() }; cm.AddUser(packet.ChannelId, memberId, newPlayer, memberPeer); // 접속된 모든 유저 정보 전달 SendIntoChannelPacket(memberPeer, memberId); // 내 정보 전달 SendLoadGame(memberPeer, memberId); } } // 새로운 채널에 파티를 추가한다. if (newChannel.GetPartyManager() .CreateParty(tempParty.LeaderId, tempParty.PartyName, out PartyInfo? createdParty, tempParty.PartyMemberIds) && createdParty != null) { // 새 채널 기존 유저들에게 파티 생성 알림 UpdatePartyPacket createNotify = new() { PartyId = createdParty.PartyId, Type = PartyUpdateType.CREATE, LeaderId = createdParty.LeaderId, PartyName = createdParty.PartyName }; BroadcastToChannel(packet.ChannelId, PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, createNotify)); } else { Log.Warning("[GameServer] 파티생성 실패 !!!"); } } private void OnExitChannel(NetPeer peer, int hashKey, byte[] payload) { ExitChannelPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; // 제거 전에 채널/플레이어 정보 저장 (브로드캐스트에 필요) int channelId = cm.HasUser(hashKey); Player? player = channelId >= 0 ? cm.GetChannel(channelId).GetPlayer(hashKey) : null; // 파티 자동 탈퇴 if (channelId >= 0) { HandlePartyLeaveOnExit(channelId, hashKey); } cm.RemoveUser(hashKey); Log.Debug("[GameServer] EXIT_CHANNEL HashKey={Key} PlayerId={PlayerId}", hashKey, packet.PlayerId); // 같은 채널 유저들에게 나갔다고 알림 if (channelId >= 0 && player != null) { SendExitChannelPacket(peer, hashKey, channelId, player); } } private void OnTransformPlayer(NetPeer peer, int hashKey, byte[] payload) { TransformPlayerPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } // 채널 내 플레이어 위치/방향 상태 갱신 Player? player = cm.GetChannel(channelId).GetPlayer(hashKey); if (player == null) { return; } player.PosX = packet.Position.X; player.PosY = packet.Position.Y; player.PosZ = packet.Position.Z; player.RotY = packet.RotY; // 같은 맵 유저들에게 위치/방향 브로드캐스트 (나 제외) byte[] data = PacketSerializer.Serialize((ushort)PacketCode.TRANSFORM_PLAYER, packet); BroadcastToMap(channelId, player.CurrentMapId, data, peer, DeliveryMethod.Unreliable); } private void OnActionPlayer(NetPeer peer, int hashKey, byte[] payload) { ActionPlayerPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } Player? player = cm.GetChannel(channelId).GetPlayer(hashKey); if (player == null) { return; } // 같은 맵 유저들에게 행동 브로드캐스트 (나 제외) byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ACTION_PLAYER, packet); BroadcastToMap(channelId, player.CurrentMapId, data, peer); } private void OnStatePlayer(NetPeer peer, int hashKey, byte[] payload) { StatePlayerPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } // 채널 내 플레이어 HP/MP 상태 갱신 Player? player = cm.GetChannel(channelId).GetPlayer(hashKey); if (player == null) { return; } player.Hp = packet.Hp; player.MaxHp = packet.MaxHp; player.Mp = packet.Mp; player.MaxMp = packet.MaxMp; // 같은 맵 유저들에게 스테이트 브로드캐스트 (나 제외) byte[] data = PacketSerializer.Serialize((ushort)PacketCode.STATE_PLAYER, packet); BroadcastToMap(channelId, player.CurrentMapId, data, peer); } private void OnRequestParty(NetPeer peer, int hashKey, byte[] payload) { RequestPartyPacket req = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } PartyManager pm = cm.GetChannel(channelId).GetPartyManager(); switch (req.Type) { case PartyUpdateType.CREATE: { if (!pm.CreateParty(hashKey, req.PartyName, out PartyInfo? party)) { SendError(peer, ErrorCode.PARTY_ALREADY_IN_PARTY); return; } UpdatePartyPacket notify = new() { PartyId = party!.PartyId, Type = PartyUpdateType.CREATE, LeaderId = party.LeaderId, PlayerId = hashKey, PartyName = party.PartyName }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } case PartyUpdateType.JOIN: { if (!pm.JoinParty(hashKey, req.PartyId, out PartyInfo? party)) { SendError(peer, ErrorCode.PARTY_JOIN_FAILED); return; } UpdatePartyPacket notify = new() { PartyId = party!.PartyId, Type = PartyUpdateType.JOIN, LeaderId = party.LeaderId, PlayerId = hashKey }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } case PartyUpdateType.LEAVE: { if (!pm.LeaveParty(hashKey, out PartyInfo? party) || party == null) { SendError(peer, ErrorCode.PARTY_NOT_IN_PARTY); return; } UpdatePartyPacket notify = new() { PartyId = party.PartyId, Type = PartyUpdateType.DELETE, LeaderId = party.LeaderId, PlayerId = hashKey }; // 파티가 남아있으면 살린다. if (party.PartyMemberIds.Count > 0) { notify.Type = PartyUpdateType.LEAVE; } byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 (탈퇴자 포함) break; } case PartyUpdateType.DELETE: { if (!pm.DeleteParty(hashKey, req.PartyId, out PartyInfo? party)) { SendError(peer, ErrorCode.PARTY_DELETE_FAILED); return; } UpdatePartyPacket notify = new() { PartyId = party!.PartyId, Type = PartyUpdateType.DELETE, LeaderId = party.LeaderId }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } case PartyUpdateType.UPDATE: { if (!pm.UpdateParty(hashKey, req.PartyId, req.PartyName, out PartyInfo? party)) { SendError(peer, ErrorCode.PARTY_UPDATE_FAILED); return; } UpdatePartyPacket notify = new() { PartyId = req.PartyId, Type = PartyUpdateType.UPDATE, LeaderId = party?.LeaderId ?? 0, PlayerId = hashKey, PartyName = req.PartyName }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } } } private void OnChat(NetPeer peer, int hashKey, byte[] payload) { ChatPacket req = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } Player? sender = cm.GetChannel(channelId).GetPlayer(hashKey); if (sender == null) { return; } // 서버에서 발신자 정보 채워줌 (클라 위조 방지) ChatPacket res = new() { Type = req.Type, SenderId = sender.PlayerId, SenderNickname = sender.Nickname, TargetId = req.TargetId, Message = req.Message }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.CHAT, res); switch (req.Type) { case ChatType.GLOBAL: // 채널 내 모든 유저 (자신 포함) BroadcastToChannel(channelId, data); Log.Debug("[Chat] GLOBAL HashKey={Key} Message={Msg}", hashKey, req.Message); break; case ChatType.PARTY: // 파티 멤버에게만 (자신 포함) PartyManager pm = cm.GetChannel(channelId).GetPartyManager(); PartyInfo? party = pm.GetPartyByPlayer(hashKey); if (party == null) { Log.Warning("[Chat] PARTY 파티 없음 HashKey={Key}", hashKey); return; } BroadcastToUsers(party.PartyMemberIds, data); Log.Debug("[Chat] PARTY HashKey={Key} PartyId={PartyId} Message={Msg}", hashKey, party.PartyId, req.Message); break; case ChatType.WHISPER: // 대상 + 발신자에게만 if (sessions.TryGetValue(req.TargetId, out NetPeer? targetPeer)) { SendTo(targetPeer, data); } else { Log.Warning("[Chat] WHISPER 대상 없음 HashKey={Key} TargetId={TargetId}", hashKey, req.TargetId); } // 자신에게도 전송 (귓말 확인용) SendTo(peer, data); Log.Debug("[Chat] WHISPER HashKey={Key} TargetId={TargetId} Message={Msg}", hashKey, req.TargetId, req.Message); break; } } private void OnChangeMap(NetPeer peer, int hashKey, byte[] payload) { ChangeMapPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } Channel.Channel channel = cm.GetChannel(channelId); Player? player = channel.GetPlayer(hashKey); if (player == null) { return; } int oldMapId = player.CurrentMapId; int newMapId = packet.MapId; // 일단 보스맵에서 로비로 원복할떄 캐싱해둔 걸로 교체 if (newMapId == -1) { if (player.PreviousMapId > 0) { // 레이드 맵 사용 종료 처리 channel.RemoveInstanceMap(oldMapId); newMapId = player.PreviousMapId; player.PreviousMapId = 0; } } if (!channel.ChangeMap(hashKey, player, newMapId)) { Log.Warning("[GameServer] CHANGE_MAP 유효하지 않은 맵 HashKey={Key} MapId={MapId}", hashKey, newMapId); return; } PlayerInfo playerInfo = ToPlayerInfo(player); // 기존 맵 유저들에게 퇴장 알림 ChangeMapPacket exitNotify = new() { MapId = oldMapId, IsAdd = false, Player = playerInfo }; BroadcastToMap(channelId, oldMapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, exitNotify)); // 새 맵 유저들에게 입장 알림 (본인 제외) ChangeMapPacket enterNotify = new() { MapId = newMapId, IsAdd = true, Player = playerInfo }; BroadcastToMap(channelId, newMapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, enterNotify), peer); // 본인에게 새 맵 플레이어 목록 전달 ChangeMapPacket response = new() { MapId = newMapId }; AMap? newMap = channel.GetMap(newMapId); if (newMap != null) { foreach ((int uid, Player p) in newMap.GetUsers()) { if (uid == hashKey) { continue; } response.Players.Add(ToPlayerInfo(p)); } } SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response)); Log.Debug("[GameServer] CHANGE_MAP HashKey={Key} OldMap={OldMapId} NewMap={MapId}", hashKey, oldMapId, newMapId); } private void OnPartyChangeMap(NetPeer peer, int hashKey, byte[] payload) { PartyChangeMapPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } Channel.Channel channel = cm.GetChannel(channelId); // 맵 유효성 체크 if (channel.GetMap(packet.MapId) == null) { Log.Warning("[GameServer] PARTY_CHANGE_MAP 유효하지 않은 맵 HashKey={Key} MapId={MapId}", hashKey, packet.MapId); return; } // 파티 확인 PartyInfo? party = channel.GetPartyManager().GetParty(packet.PartyId); if (party == null) { Log.Warning("[GameServer] PARTY_CHANGE_MAP 파티 없음 HashKey={Key} PartyId={PartyId}", hashKey, packet.PartyId); return; } // 파티원 전체 맵 이동 + 각자에게 알림 foreach (int memberId in party.PartyMemberIds) { Player? memberPlayer = channel.GetPlayer(memberId); if (memberPlayer == null) { continue; } int oldMapId = memberPlayer.CurrentMapId; channel.ChangeMap(memberId, memberPlayer, packet.MapId); if (!sessions.TryGetValue(memberId, out NetPeer? memberPeer)) { continue; } PlayerInfo memberInfo = ToPlayerInfo(memberPlayer); // 기존 맵 유저들에게 퇴장 알림 ChangeMapPacket exitNotify = new() { MapId = oldMapId, IsAdd = false, Player = memberInfo }; BroadcastToMap(channelId, oldMapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, exitNotify)); // 새 맵 유저들에게 입장 알림 (본인 제외) ChangeMapPacket enterNotify = new() { MapId = packet.MapId, IsAdd = true, Player = memberInfo }; BroadcastToMap(channelId, packet.MapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, enterNotify), memberPeer); // 본인에게 새 맵 플레이어 목록 전달 ChangeMapPacket response = new() { MapId = packet.MapId }; AMap? newMap = channel.GetMap(packet.MapId); if (newMap != null) { foreach ((int uid, Player p) in newMap.GetUsers()) { if (uid == memberId) { continue; } response.Players.Add(ToPlayerInfo(p)); } } SendTo(memberPeer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response)); } Log.Debug("[GameServer] PARTY_CHANGE_MAP HashKey={Key} PartyId={PartyId} MapId={MapId}", hashKey, packet.PartyId, packet.MapId); } private async void OnIntoBossRaid(NetPeer peer, int hashKey, byte[] payload) { IntoBossRaidPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } Channel.Channel channel = cm.GetChannel(channelId); // 파티 조회 PartyInfo? party = channel.GetPartyManager().GetPartyByPlayer(hashKey); if (party == null) { Log.Warning("[GameServer] INTO_BOSS_RAID 파티 없음 HashKey={Key}", hashKey); SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.INTO_BOSS_RAID, new IntoBossRaidPacket { RaidId = packet.RaidId, IsSuccess = false })); return; } // 파티장만 요청 가능 if (party.LeaderId != hashKey) { Log.Warning("[GameServer] INTO_BOSS_RAID 파티장 아님 HashKey={Key} LeaderId={LeaderId}", hashKey, party.LeaderId); SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.INTO_BOSS_RAID, new IntoBossRaidPacket { RaidId = packet.RaidId, IsSuccess = false })); return; } // API로 접속 체크 List userNames = new List(); foreach (int memberId in party.PartyMemberIds) { Player? memberPlayer = channel.GetPlayer(memberId); if (memberPlayer != null) { userNames.Add(memberPlayer.Nickname); } } BossRaidResult? result = await RestApi.Instance.BossRaidAccesssAsync(userNames, 1); // 입장 실패 if (result == null || result.BossId <= 0) { SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.INTO_BOSS_RAID, new IntoBossRaidPacket { RaidId = -1, IsSuccess = false })); Log.Debug("[GameServer] INTO_BOSS_RAID HashKey={Key} PartyId={PartyId} AssignedRaidMapId={RaidId} Failed", hashKey, party.PartyId, -1); return; } // 레이드 맵 할당 (미사용 맵 탐색 → 없으면 동적 생성) int assignedRaidMapId = channel.GetOrCreateAvailableRaidMap(); // 진행중 맵으로 등록 channel.AddInstanceMap(assignedRaidMapId); // 파티원 전체 레이드 맵으로 이동 + 각자에게 알림 foreach (int memberId in party.PartyMemberIds) { Player? memberPlayer = channel.GetPlayer(memberId); if (memberPlayer == null) { continue; } // 이전 맵 캐싱 (레이드 종료 후 복귀용) memberPlayer.PreviousMapId = memberPlayer.CurrentMapId; int oldMapId = memberPlayer.CurrentMapId; channel.ChangeMap(memberId, memberPlayer, assignedRaidMapId); if (!sessions.TryGetValue(memberId, out NetPeer? memberPeer)) { continue; } PlayerInfo memberInfo = ToPlayerInfo(memberPlayer); // 기존 맵 유저들에게 퇴장 알림 ChangeMapPacket exitNotify = new() { MapId = oldMapId, IsAdd = false, Player = memberInfo }; BroadcastToMap(channelId, oldMapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, exitNotify)); // 새 맵 유저들에게 입장 알림 (본인 제외) ChangeMapPacket enterNotify = new() { MapId = assignedRaidMapId, IsAdd = true, Player = memberInfo }; BroadcastToMap(channelId, assignedRaidMapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, enterNotify), memberPeer); // 본인에게 새 맵 플레이어 목록 전달 ChangeMapPacket response = new() { MapId = assignedRaidMapId }; AMap? raidMap = channel.GetMap(assignedRaidMapId); if (raidMap != null) { foreach ((int uid, Player p) in raidMap.GetUsers()) { if (uid != memberId) { response.Players.Add(ToPlayerInfo(p)); } } } SendTo(memberPeer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response)); // 각 멤버에게 개별 토큰과 함께 레이드 이동 알림 string? memberToken = null; result.Tokens?.TryGetValue(memberPlayer.Nickname, out memberToken); SendTo(memberPeer, PacketSerializer.Serialize((ushort)PacketCode.INTO_BOSS_RAID, new IntoBossRaidPacket { RaidId = assignedRaidMapId, IsSuccess = true, Session = result.SessionName, Token = memberToken })); } Log.Debug("[GameServer] INTO_BOSS_RAID HashKey={Key} PartyId={PartyId} AssignedRaidMapId={RaidId}", hashKey, party.PartyId, assignedRaidMapId); } // 채널 퇴장/연결 해제 시 파티 자동 탈퇴 처리 private void HandlePartyLeaveOnExit(int channelId, int hashKey) { PartyManager pm = ChannelManager.Instance.GetChannel(channelId).GetPartyManager(); int? partyId = pm.GetPartyByPlayer(hashKey)?.PartyId; if (partyId == null) { return; // 파티에 없으면 무시 } pm.LeaveParty(hashKey, out PartyInfo? remaining); // 0명 → DELETE, 남은 멤버 있음 → LEAVE UpdatePartyPacket notify = remaining != null ? new UpdatePartyPacket { PartyId = partyId.Value, Type = PartyUpdateType.LEAVE, LeaderId = remaining.LeaderId, PlayerId = hashKey } : new UpdatePartyPacket { PartyId = partyId.Value, Type = PartyUpdateType.DELETE, LeaderId = -1, PlayerId = hashKey }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); } private void SendError(NetPeer peer, ErrorCode code) { ErrorPacket err = new() { Code = code }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ERROR, err); SendTo(peer, data); } private void BroadcastToUsers(IEnumerable userIds, byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { foreach (int userId in userIds) { if (sessions.TryGetValue(userId, out NetPeer? targetPeer)) { SendTo(targetPeer, data, method); } } } }