From 0ebe269146feaf3c2a5c3a5b68f10b63fdf5467a Mon Sep 17 00:00:00 2001 From: qornwh1 Date: Thu, 12 Mar 2026 13:23:30 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=EB=B3=B4=EC=8A=A4=EC=A0=84=20?= =?UTF-8?q?=EC=B1=84=EB=84=90=20=EC=83=9D=EC=84=B1,=20=ED=8C=8C=ED=8B=B0?= =?UTF-8?q?=20=ED=95=A8=EA=BB=98=20=EC=B1=84=EB=84=90=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EchoClientTester/Packet/PacketBody.cs | 54 +++++-- .../EchoClientTester/Packet/PacketHeader.cs | 3 + .../MMOserver/Game/Channel/ChannelManager.cs | 23 ++- MMOTestServer/MMOserver/Game/GameServer.cs | 134 +++++++++++++++++- MMOTestServer/MMOserver/Game/Party/Party.cs | 17 +++ .../MMOserver/Game/Party/PartyManager.cs | 9 +- MMOTestServer/MMOserver/Packet/PacketBody.cs | 52 +++++-- .../MMOserver/Packet/PacketHeader.cs | 3 + 8 files changed, 268 insertions(+), 27 deletions(-) diff --git a/ClientTester/EchoClientTester/Packet/PacketBody.cs b/ClientTester/EchoClientTester/Packet/PacketBody.cs index 3b0a056..5adae64 100644 --- a/ClientTester/EchoClientTester/Packet/PacketBody.cs +++ b/ClientTester/EchoClientTester/Packet/PacketBody.cs @@ -243,7 +243,9 @@ public class PartyInfoData } } -// INTO_CHANNEL 클라->서버: 입장할 채널 ID / 서버->클라: 채널 내 나 이외 플레이어 목록 +// INTO_CHANNEL +// 클라->서버: 입장할 채널 ID +// 서버->클라: 채널 내 나 이외 플레이어 목록 [ProtoContract] public class IntoChannelPacket { @@ -269,6 +271,40 @@ public class IntoChannelPacket } = new List(); // 서버->클라: 채널 내 파티 목록 } +// 파티원 모두 채널이동 +// 클라->서버: 입장할 채널 ID +[ProtoContract] +public class IntoChannelPartyPacket +{ + [ProtoMember(1)] + public int ChannelId + { + get; + set; + } // 클라->서버: 입장할 채널 ID + + [ProtoMember(2)] + public List Players + { + get; + set; + } = new List(); // 서버->클라: 채널 내 플레이어 목록 + + [ProtoMember(3)] + public List Parties + { + get; + set; + } = new List(); // 서버->클라: 채널 내 파티 목록 + + [ProtoMember(4)] + public int PartyId + { + get; + set; + } +} + // UPDATE_CHANNEL_USER 유저 접속/나감 [ProtoContract] public class UpdateChannelUserPacket @@ -564,9 +600,10 @@ public enum ErrorCode : int { // 파티 (10021~) PARTY_ALREADY_IN_PARTY = 10021, - PARTY_JOIN_FAILED = 10022, - PARTY_NOT_IN_PARTY = 10023, - PARTY_DELETE_FAILED = 10024, + PARTY_JOIN_FAILED = 10022, + PARTY_NOT_IN_PARTY = 10023, + PARTY_DELETE_FAILED = 10024, + PARTY_UPDATE_FAILED = 10025, } // ERROR (서버 -> 클라) @@ -590,7 +627,8 @@ public enum PartyUpdateType CREATE, DELETE, JOIN, - LEAVE + LEAVE, + UPDATE } // REQUEST_PARTY (클라 -> 서버) - CREATE: PartyName 사용 / JOIN·LEAVE·DELETE: PartyId 사용 @@ -625,9 +663,9 @@ public class RequestPartyPacket public enum ChatType { - GLOBAL, // 전체 채널 - PARTY, // 파티원 - WHISPER // 귓말 + GLOBAL, // 전체 채널 + PARTY, // 파티원 + WHISPER // 귓말 } // CHAT (클라 -> 서버 & 서버 -> 클라) diff --git a/ClientTester/EchoClientTester/Packet/PacketHeader.cs b/ClientTester/EchoClientTester/Packet/PacketHeader.cs index f011e03..982f571 100644 --- a/ClientTester/EchoClientTester/Packet/PacketHeader.cs +++ b/ClientTester/EchoClientTester/Packet/PacketHeader.cs @@ -21,6 +21,9 @@ public enum PacketCode : ushort // 나 채널 접속 (클라 -> 서버) INTO_CHANNEL, + // 파티 채널 접속 (클라 -> 서버) + INTO_CHANNEL_PARTY, + // 새로운 유저 채널 접속 (서버 -> 클라) / 유저 채널 나감 (서버 -> 클라) UPDATE_CHANNEL_USER, diff --git a/MMOTestServer/MMOserver/Game/Channel/ChannelManager.cs b/MMOTestServer/MMOserver/Game/Channel/ChannelManager.cs index f2417a7..3e6e6a8 100644 --- a/MMOTestServer/MMOserver/Game/Channel/ChannelManager.cs +++ b/MMOTestServer/MMOserver/Game/Channel/ChannelManager.cs @@ -6,7 +6,11 @@ namespace MMOserver.Game.Channel; public class ChannelManager : Singleton { // 채널 관리 - private List channels = new List(); + private Dictionary channels = new Dictionary(); + + // 보스 레이드 채널 + private readonly int bossChannelStart = 10000; + private readonly int bossChannelSize = 10; // 채널별 유저 관리 (유저 key, 채널 val) private Dictionary connectUsers = new Dictionary(); @@ -18,9 +22,16 @@ public class ChannelManager : Singleton public void Initializer(int channelSize = 1) { - for (int i = 0; i <= channelSize; i++) + for (int i = 1; i <= channelSize; i++) { - channels.Add(new Channel(i)); + channels.Add(i, new Channel(i)); + } + + // 보스 채널 생성 + for (int i = 1; i <= bossChannelSize; i++) + { + int bossChannel = i + bossChannelStart; + channels.Add(bossChannel, new Channel(bossChannel)); } } @@ -29,15 +40,15 @@ public class ChannelManager : Singleton return channels[channelId]; } - public List GetChannels() + public Dictionary GetChannels() { return channels; } public void AddUser(int channelId, int userId, Player player, NetPeer peer) { - // 유저 추가 - connectUsers.Add(userId, channelId); + // 유저 추가 (채널 이동 시 기존 매핑 덮어쓰기 허용) + connectUsers[userId] = channelId; // 채널에 유저 + peer 추가 channels[channelId].AddUser(userId, player, peer); } diff --git a/MMOTestServer/MMOserver/Game/GameServer.cs b/MMOTestServer/MMOserver/Game/GameServer.cs index 728201b..5bec060 100644 --- a/MMOTestServer/MMOserver/Game/GameServer.cs +++ b/MMOTestServer/MMOserver/Game/GameServer.cs @@ -22,6 +22,7 @@ public class GameServer : ServerBase 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, @@ -208,7 +209,7 @@ public class GameServer : ServerBase private void SendLoadChannelPacket(NetPeer peer, int hashKey) { LoadChannelPacket loadChannelPacket = new LoadChannelPacket(); - foreach (Channel.Channel channel in ChannelManager.Instance.GetChannels()) + foreach (Channel.Channel channel in ChannelManager.Instance.GetChannels().Values) { if (channel.ChannelId <= 0) { @@ -376,13 +377,34 @@ public class GameServer : ServerBase IntoChannelPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; - Channel.Channel channel = cm.GetChannel(packet.ChannelId); + + // 이전에 다른 채널에 있었는지 체크 + 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 (channel.UserCount >= channel.UserCountMax) + if (newChannel.UserCount >= newChannel.UserCountMax) { Log.Warning("[GameServer] INTO_CHANNEL 채널 인원 초과 HashKey={Key} ChannelId={ChannelId} UserCount={Count}/{Max}", - hashKey, packet.ChannelId, channel.UserCount, channel.UserCountMax); + hashKey, packet.ChannelId, newChannel.UserCount, newChannel.UserCountMax); byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, new IntoChannelPacket { ChannelId = -1 }); SendTo(peer, full); @@ -407,6 +429,110 @@ public class GameServer : ServerBase SendLoadGame(peer, hashKey); } + 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 newParty = new PartyInfo(); + newParty.DeepCopySemi(preParty); + + // 최대 인원 체크 + if (newChannel.UserCount + newParty.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 UpdateChannelUserPacket + { + Players = ToPlayerInfo(player), + IsAdd = false + }; + byte[] exitData = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, exitNotify); + BroadcastToChannel(preChannelId, exitData); + + // 이전 채널에서 제거 + preChannel.RemoveUser(memberId); + + // 현재 존재하는 파티원만 추가한다. + newParty.PartyMemberIds.Add(memberId); + } + } + + // 이전채널에서 파티를 지운다. + preChannel.GetPartyManager().DeleteParty(hashKey, packet.PartyId, out preParty); + UpdatePartyPacket notify = new UpdatePartyPacket + { + PartyId = preParty!.PartyId, + Type = PartyUpdateType.DELETE, + LeaderId = preParty.LeaderId, + }; + BroadcastToChannel(preChannelId, + PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify)); // 채널 전체 파티 목록 갱신 + + // 새로운 채널에 파티원 넣기 + foreach (int memberId in newParty.PartyMemberIds) + { + sessions.TryGetValue(memberId, out NetPeer? memberPeer); + + if (memberPeer != null) + { + // 새 채널에 유저 추가 + Player newPlayer = new Player + { + HashKey = memberId, + PlayerId = memberId, + Nickname = memberId.ToString() + }; + cm.AddUser(packet.ChannelId, memberId, newPlayer, memberPeer); + + // 접속된 모든 유저 정보 전달 + SendIntoChannelPacket(memberPeer, memberId); + + // 내 정보 전달 + SendLoadGame(memberPeer, memberId); + } + } + + // 새로운 채널에 파티를 추가한다. + newChannel.GetPartyManager().CreateParty(newParty.LeaderId, newParty.PartyName, out newParty, newParty.PartyMemberIds); + + // 새 채널 기존 유저들에게 파티 생성 알림 + UpdatePartyPacket createNotify = new UpdatePartyPacket + { + PartyId = newParty.PartyId, + Type = PartyUpdateType.CREATE, + LeaderId = newParty.LeaderId, + PartyName = newParty.PartyName, + }; + BroadcastToChannel(packet.ChannelId, PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, createNotify)); + } + private void OnExitChannel(NetPeer peer, int hashKey, byte[] payload) { ExitChannelPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); diff --git a/MMOTestServer/MMOserver/Game/Party/Party.cs b/MMOTestServer/MMOserver/Game/Party/Party.cs index 8c96a15..c027a13 100644 --- a/MMOTestServer/MMOserver/Game/Party/Party.cs +++ b/MMOTestServer/MMOserver/Game/Party/Party.cs @@ -32,4 +32,21 @@ public class PartyInfo { return PartyMemberIds.Count; } + + public void DeepCopy(PartyInfo other) + { + this.PartyId = other.PartyId; + this.PartyName = other.PartyName; + this.LeaderId = other.LeaderId; + this.PartyMemberIds.Clear(); + this.PartyMemberIds.AddRange(other.PartyMemberIds); + } + + public void DeepCopySemi(PartyInfo other) + { + this.PartyId = other.PartyId; + this.PartyName = other.PartyName; + this.LeaderId = other.LeaderId; + this.PartyMemberIds = new List(); + } } diff --git a/MMOTestServer/MMOserver/Game/Party/PartyManager.cs b/MMOTestServer/MMOserver/Game/Party/PartyManager.cs index d094eb1..857e020 100644 --- a/MMOTestServer/MMOserver/Game/Party/PartyManager.cs +++ b/MMOTestServer/MMOserver/Game/Party/PartyManager.cs @@ -13,7 +13,7 @@ public class PartyManager private readonly Dictionary playerPartyMap = new(); // 파티 생성 - public bool CreateParty(int leaderId, string partyName, out PartyInfo? party) + public bool CreateParty(int leaderId, string partyName, out PartyInfo? party, List? memeberIds = null) { party = null; @@ -23,11 +23,18 @@ public class PartyManager } int partyId = partyUuidGenerator.Create(); + + if (memeberIds == null) + { + memeberIds = new List(); + } + party = new PartyInfo { PartyId = partyId, LeaderId = leaderId, PartyName = partyName, + PartyMemberIds = memeberIds }; party.PartyMemberIds.Add(leaderId); diff --git a/MMOTestServer/MMOserver/Packet/PacketBody.cs b/MMOTestServer/MMOserver/Packet/PacketBody.cs index 76b52f9..6903fad 100644 --- a/MMOTestServer/MMOserver/Packet/PacketBody.cs +++ b/MMOTestServer/MMOserver/Packet/PacketBody.cs @@ -243,7 +243,9 @@ public class PartyInfoData } } -// INTO_CHANNEL 클라->서버: 입장할 채널 ID / 서버->클라: 채널 내 나 이외 플레이어 목록 +// INTO_CHANNEL +// 클라->서버: 입장할 채널 ID +// 서버->클라: 채널 내 나 이외 플레이어 목록 [ProtoContract] public class IntoChannelPacket { @@ -269,6 +271,40 @@ public class IntoChannelPacket } = new List(); // 서버->클라: 채널 내 파티 목록 } +// 파티원 모두 채널이동 +// 클라->서버: 입장할 채널 ID +[ProtoContract] +public class IntoChannelPartyPacket +{ + [ProtoMember(1)] + public int ChannelId + { + get; + set; + } // 클라->서버: 입장할 채널 ID + + [ProtoMember(2)] + public List Players + { + get; + set; + } = new List(); // 서버->클라: 채널 내 플레이어 목록 + + [ProtoMember(3)] + public List Parties + { + get; + set; + } = new List(); // 서버->클라: 채널 내 파티 목록 + + [ProtoMember(4)] + public int PartyId + { + get; + set; + } +} + // UPDATE_CHANNEL_USER 유저 접속/나감 [ProtoContract] public class UpdateChannelUserPacket @@ -564,10 +600,10 @@ public enum ErrorCode : int { // 파티 (10021~) PARTY_ALREADY_IN_PARTY = 10021, - PARTY_JOIN_FAILED = 10022, - PARTY_NOT_IN_PARTY = 10023, - PARTY_DELETE_FAILED = 10024, - PARTY_UPDATE_FAILED = 10025, + PARTY_JOIN_FAILED = 10022, + PARTY_NOT_IN_PARTY = 10023, + PARTY_DELETE_FAILED = 10024, + PARTY_UPDATE_FAILED = 10025, } // ERROR (서버 -> 클라) @@ -627,9 +663,9 @@ public class RequestPartyPacket public enum ChatType { - GLOBAL, // 전체 채널 - PARTY, // 파티원 - WHISPER // 귓말 + GLOBAL, // 전체 채널 + PARTY, // 파티원 + WHISPER // 귓말 } // CHAT (클라 -> 서버 & 서버 -> 클라) diff --git a/MMOTestServer/MMOserver/Packet/PacketHeader.cs b/MMOTestServer/MMOserver/Packet/PacketHeader.cs index ab11ccc..09b703d 100644 --- a/MMOTestServer/MMOserver/Packet/PacketHeader.cs +++ b/MMOTestServer/MMOserver/Packet/PacketHeader.cs @@ -21,6 +21,9 @@ public enum PacketCode : ushort // 나 채널 접속 (클라 -> 서버) INTO_CHANNEL, + // 파티 채널 접속 (클라 -> 서버) + INTO_CHANNEL_PARTY, + // 새로운 유저 채널 접속 (서버 -> 클라) / 유저 채널 나감 (서버 -> 클라) UPDATE_CHANNEL_USER,