feat : 보스전 채널 생성, 파티 함께 채널 이동 구현

This commit is contained in:
qornwh1
2026-03-12 13:23:30 +09:00
parent 4956a2e26d
commit 0ebe269146
8 changed files with 268 additions and 27 deletions

View File

@@ -243,7 +243,9 @@ public class PartyInfoData
} }
} }
// INTO_CHANNEL 클라->서버: 입장할 채널 ID / 서버->클라: 채널 내 나 이외 플레이어 목록 // INTO_CHANNEL
// 클라->서버: 입장할 채널 ID
// 서버->클라: 채널 내 나 이외 플레이어 목록
[ProtoContract] [ProtoContract]
public class IntoChannelPacket public class IntoChannelPacket
{ {
@@ -269,6 +271,40 @@ public class IntoChannelPacket
} = new List<PartyInfoData>(); // 서버->클라: 채널 내 파티 목록 } = new List<PartyInfoData>(); // 서버->클라: 채널 내 파티 목록
} }
// 파티원 모두 채널이동
// 클라->서버: 입장할 채널 ID
[ProtoContract]
public class IntoChannelPartyPacket
{
[ProtoMember(1)]
public int ChannelId
{
get;
set;
} // 클라->서버: 입장할 채널 ID
[ProtoMember(2)]
public List<PlayerInfo> Players
{
get;
set;
} = new List<PlayerInfo>(); // 서버->클라: 채널 내 플레이어 목록
[ProtoMember(3)]
public List<PartyInfoData> Parties
{
get;
set;
} = new List<PartyInfoData>(); // 서버->클라: 채널 내 파티 목록
[ProtoMember(4)]
public int PartyId
{
get;
set;
}
}
// UPDATE_CHANNEL_USER 유저 접속/나감 // UPDATE_CHANNEL_USER 유저 접속/나감
[ProtoContract] [ProtoContract]
public class UpdateChannelUserPacket public class UpdateChannelUserPacket
@@ -564,9 +600,10 @@ public enum ErrorCode : int
{ {
// 파티 (10021~) // 파티 (10021~)
PARTY_ALREADY_IN_PARTY = 10021, PARTY_ALREADY_IN_PARTY = 10021,
PARTY_JOIN_FAILED = 10022, PARTY_JOIN_FAILED = 10022,
PARTY_NOT_IN_PARTY = 10023, PARTY_NOT_IN_PARTY = 10023,
PARTY_DELETE_FAILED = 10024, PARTY_DELETE_FAILED = 10024,
PARTY_UPDATE_FAILED = 10025,
} }
// ERROR (서버 -> 클라) // ERROR (서버 -> 클라)
@@ -590,7 +627,8 @@ public enum PartyUpdateType
CREATE, CREATE,
DELETE, DELETE,
JOIN, JOIN,
LEAVE LEAVE,
UPDATE
} }
// REQUEST_PARTY (클라 -> 서버) - CREATE: PartyName 사용 / JOIN·LEAVE·DELETE: PartyId 사용 // REQUEST_PARTY (클라 -> 서버) - CREATE: PartyName 사용 / JOIN·LEAVE·DELETE: PartyId 사용
@@ -625,9 +663,9 @@ public class RequestPartyPacket
public enum ChatType public enum ChatType
{ {
GLOBAL, // 전체 채널 GLOBAL, // 전체 채널
PARTY, // 파티원 PARTY, // 파티원
WHISPER // 귓말 WHISPER // 귓말
} }
// CHAT (클라 -> 서버 & 서버 -> 클라) // CHAT (클라 -> 서버 & 서버 -> 클라)

View File

@@ -21,6 +21,9 @@ public enum PacketCode : ushort
// 나 채널 접속 (클라 -> 서버) // 나 채널 접속 (클라 -> 서버)
INTO_CHANNEL, INTO_CHANNEL,
// 파티 채널 접속 (클라 -> 서버)
INTO_CHANNEL_PARTY,
// 새로운 유저 채널 접속 (서버 -> 클라) / 유저 채널 나감 (서버 -> 클라) // 새로운 유저 채널 접속 (서버 -> 클라) / 유저 채널 나감 (서버 -> 클라)
UPDATE_CHANNEL_USER, UPDATE_CHANNEL_USER,

View File

@@ -6,7 +6,11 @@ namespace MMOserver.Game.Channel;
public class ChannelManager : Singleton<ChannelManager> public class ChannelManager : Singleton<ChannelManager>
{ {
// 채널 관리 // 채널 관리
private List<Channel> channels = new List<Channel>(); private Dictionary<int, Channel> channels = new Dictionary<int, Channel>();
// 보스 레이드 채널
private readonly int bossChannelStart = 10000;
private readonly int bossChannelSize = 10;
// 채널별 유저 관리 (유저 key, 채널 val) // 채널별 유저 관리 (유저 key, 채널 val)
private Dictionary<int, int> connectUsers = new Dictionary<int, int>(); private Dictionary<int, int> connectUsers = new Dictionary<int, int>();
@@ -18,9 +22,16 @@ public class ChannelManager : Singleton<ChannelManager>
public void Initializer(int channelSize = 1) 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<ChannelManager>
return channels[channelId]; return channels[channelId];
} }
public List<Channel> GetChannels() public Dictionary<int, Channel> GetChannels()
{ {
return channels; return channels;
} }
public void AddUser(int channelId, int userId, Player player, NetPeer peer) public void AddUser(int channelId, int userId, Player player, NetPeer peer)
{ {
// 유저 추가 // 유저 추가 (채널 이동 시 기존 매핑 덮어쓰기 허용)
connectUsers.Add(userId, channelId); connectUsers[userId] = channelId;
// 채널에 유저 + peer 추가 // 채널에 유저 + peer 추가
channels[channelId].AddUser(userId, player, peer); channels[channelId].AddUser(userId, player, peer);
} }

View File

@@ -22,6 +22,7 @@ public class GameServer : ServerBase
packetHandlers = new Dictionary<ushort, Action<NetPeer, int, byte[]>> packetHandlers = new Dictionary<ushort, Action<NetPeer, int, byte[]>>
{ {
[(ushort)PacketCode.INTO_CHANNEL] = OnIntoChannel, [(ushort)PacketCode.INTO_CHANNEL] = OnIntoChannel,
[(ushort)PacketCode.INTO_CHANNEL_PARTY] = OnIntoChannelParty,
[(ushort)PacketCode.EXIT_CHANNEL] = OnExitChannel, [(ushort)PacketCode.EXIT_CHANNEL] = OnExitChannel,
[(ushort)PacketCode.TRANSFORM_PLAYER] = OnTransformPlayer, [(ushort)PacketCode.TRANSFORM_PLAYER] = OnTransformPlayer,
[(ushort)PacketCode.ACTION_PLAYER] = OnActionPlayer, [(ushort)PacketCode.ACTION_PLAYER] = OnActionPlayer,
@@ -208,7 +209,7 @@ public class GameServer : ServerBase
private void SendLoadChannelPacket(NetPeer peer, int hashKey) private void SendLoadChannelPacket(NetPeer peer, int hashKey)
{ {
LoadChannelPacket loadChannelPacket = new LoadChannelPacket(); 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) if (channel.ChannelId <= 0)
{ {
@@ -376,13 +377,34 @@ public class GameServer : ServerBase
IntoChannelPacket packet = Serializer.Deserialize<IntoChannelPacket>(new ReadOnlyMemory<byte>(payload)); IntoChannelPacket packet = Serializer.Deserialize<IntoChannelPacket>(new ReadOnlyMemory<byte>(payload));
ChannelManager cm = ChannelManager.Instance; 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}", 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<IntoChannelPacket>((ushort)PacketCode.INTO_CHANNEL, byte[] full = PacketSerializer.Serialize<IntoChannelPacket>((ushort)PacketCode.INTO_CHANNEL,
new IntoChannelPacket { ChannelId = -1 }); new IntoChannelPacket { ChannelId = -1 });
SendTo(peer, full); SendTo(peer, full);
@@ -407,6 +429,110 @@ public class GameServer : ServerBase
SendLoadGame(peer, hashKey); SendLoadGame(peer, hashKey);
} }
private void OnIntoChannelParty(NetPeer peer, int hashKey, byte[] payload)
{
IntoChannelPartyPacket packet = Serializer.Deserialize<IntoChannelPartyPacket>(new ReadOnlyMemory<byte>(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<IntoChannelPacket>((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<UpdateChannelUserPacket>((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<UpdatePartyPacket>((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<UpdatePartyPacket>((ushort)PacketCode.UPDATE_PARTY, createNotify));
}
private void OnExitChannel(NetPeer peer, int hashKey, byte[] payload) private void OnExitChannel(NetPeer peer, int hashKey, byte[] payload)
{ {
ExitChannelPacket packet = Serializer.Deserialize<ExitChannelPacket>(new ReadOnlyMemory<byte>(payload)); ExitChannelPacket packet = Serializer.Deserialize<ExitChannelPacket>(new ReadOnlyMemory<byte>(payload));

View File

@@ -32,4 +32,21 @@ public class PartyInfo
{ {
return PartyMemberIds.Count; 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<int>();
}
} }

View File

@@ -13,7 +13,7 @@ public class PartyManager
private readonly Dictionary<int, int> playerPartyMap = new(); private readonly Dictionary<int, int> playerPartyMap = new();
// 파티 생성 // 파티 생성
public bool CreateParty(int leaderId, string partyName, out PartyInfo? party) public bool CreateParty(int leaderId, string partyName, out PartyInfo? party, List<int>? memeberIds = null)
{ {
party = null; party = null;
@@ -23,11 +23,18 @@ public class PartyManager
} }
int partyId = partyUuidGenerator.Create(); int partyId = partyUuidGenerator.Create();
if (memeberIds == null)
{
memeberIds = new List<int>();
}
party = new PartyInfo party = new PartyInfo
{ {
PartyId = partyId, PartyId = partyId,
LeaderId = leaderId, LeaderId = leaderId,
PartyName = partyName, PartyName = partyName,
PartyMemberIds = memeberIds
}; };
party.PartyMemberIds.Add(leaderId); party.PartyMemberIds.Add(leaderId);

View File

@@ -243,7 +243,9 @@ public class PartyInfoData
} }
} }
// INTO_CHANNEL 클라->서버: 입장할 채널 ID / 서버->클라: 채널 내 나 이외 플레이어 목록 // INTO_CHANNEL
// 클라->서버: 입장할 채널 ID
// 서버->클라: 채널 내 나 이외 플레이어 목록
[ProtoContract] [ProtoContract]
public class IntoChannelPacket public class IntoChannelPacket
{ {
@@ -269,6 +271,40 @@ public class IntoChannelPacket
} = new List<PartyInfoData>(); // 서버->클라: 채널 내 파티 목록 } = new List<PartyInfoData>(); // 서버->클라: 채널 내 파티 목록
} }
// 파티원 모두 채널이동
// 클라->서버: 입장할 채널 ID
[ProtoContract]
public class IntoChannelPartyPacket
{
[ProtoMember(1)]
public int ChannelId
{
get;
set;
} // 클라->서버: 입장할 채널 ID
[ProtoMember(2)]
public List<PlayerInfo> Players
{
get;
set;
} = new List<PlayerInfo>(); // 서버->클라: 채널 내 플레이어 목록
[ProtoMember(3)]
public List<PartyInfoData> Parties
{
get;
set;
} = new List<PartyInfoData>(); // 서버->클라: 채널 내 파티 목록
[ProtoMember(4)]
public int PartyId
{
get;
set;
}
}
// UPDATE_CHANNEL_USER 유저 접속/나감 // UPDATE_CHANNEL_USER 유저 접속/나감
[ProtoContract] [ProtoContract]
public class UpdateChannelUserPacket public class UpdateChannelUserPacket
@@ -564,10 +600,10 @@ public enum ErrorCode : int
{ {
// 파티 (10021~) // 파티 (10021~)
PARTY_ALREADY_IN_PARTY = 10021, PARTY_ALREADY_IN_PARTY = 10021,
PARTY_JOIN_FAILED = 10022, PARTY_JOIN_FAILED = 10022,
PARTY_NOT_IN_PARTY = 10023, PARTY_NOT_IN_PARTY = 10023,
PARTY_DELETE_FAILED = 10024, PARTY_DELETE_FAILED = 10024,
PARTY_UPDATE_FAILED = 10025, PARTY_UPDATE_FAILED = 10025,
} }
// ERROR (서버 -> 클라) // ERROR (서버 -> 클라)
@@ -627,9 +663,9 @@ public class RequestPartyPacket
public enum ChatType public enum ChatType
{ {
GLOBAL, // 전체 채널 GLOBAL, // 전체 채널
PARTY, // 파티원 PARTY, // 파티원
WHISPER // 귓말 WHISPER // 귓말
} }
// CHAT (클라 -> 서버 & 서버 -> 클라) // CHAT (클라 -> 서버 & 서버 -> 클라)

View File

@@ -21,6 +21,9 @@ public enum PacketCode : ushort
// 나 채널 접속 (클라 -> 서버) // 나 채널 접속 (클라 -> 서버)
INTO_CHANNEL, INTO_CHANNEL,
// 파티 채널 접속 (클라 -> 서버)
INTO_CHANNEL_PARTY,
// 새로운 유저 채널 접속 (서버 -> 클라) / 유저 채널 나감 (서버 -> 클라) // 새로운 유저 채널 접속 (서버 -> 클라) / 유저 채널 나감 (서버 -> 클라)
UPDATE_CHANNEL_USER, UPDATE_CHANNEL_USER,