feat : 보스 레이드 입장 메시지 기능 추가

This commit is contained in:
qornwh1
2026-03-16 17:55:08 +09:00
parent 943302c2f1
commit f6b378cad7
7 changed files with 312 additions and 14 deletions

View File

@@ -35,7 +35,7 @@ public class RestApi : Singleton<RestApi>
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Log.Warning("[RestApi] 토큰 인증 실패 (401)");
return null;
return "";
}
response.EnsureSuccessStatusCode();
@@ -56,7 +56,7 @@ public class RestApi : Singleton<RestApi>
}
}
return null;
return "";
}
private sealed class AuthVerifyResponse
@@ -68,4 +68,94 @@ public class RestApi : Singleton<RestApi>
set;
}
}
// 레이드 채널 접속 여부 체크
// 성공 시 sessionName 반환, 실패/거절 시 null 반환
public async Task<bool?> BossRaidAccesssAsync(List<string> userNames, int bossId)
{
string url = AppConfig.RestApi.BaseUrl + "/api/internal/bossraid/entry";
for (int attempt = 1; attempt <= MAX_RETRY; attempt++)
{
try
{
HttpResponseMessage response = await httpClient.PostAsJsonAsync(url, new { usernames = userNames, bossId });
// 401: API 키 인증 실패
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Log.Warning("[RestApi] 보스 레이드 접속 인증 실패 (401)");
return false;
}
// 400: 입장 조건 미충족 (레벨 부족, 이미 진행중 등)
if (response.StatusCode == HttpStatusCode.BadRequest)
{
Log.Warning("[RestApi] 보스 레이드 입장 거절 (400) BossId={BossId}", bossId);
return false;
}
response.EnsureSuccessStatusCode();
BossRaidAccessResponse? result = await response.Content.ReadFromJsonAsync<BossRaidAccessResponse>();
return result?.BossId > 0 ? true : false;
}
catch (Exception ex) when (attempt < MAX_RETRY)
{
Log.Warning("[RestApi] 보스 레이드 통신 실패 (시도 {Attempt}/{Max}): {Message}", attempt, MAX_RETRY, ex.Message);
await Task.Delay(RETRY_DELAY);
}
catch (Exception ex)
{
Log.Error("[RestApi] 보스 레이드 최종 통신 실패 ({Max}회 시도): {Message}", MAX_RETRY, ex.Message);
}
}
return false;
}
private sealed class BossRaidAccessResponse
{
[JsonPropertyName("roomId")]
public int RoomId
{
get;
set;
}
[JsonPropertyName("sessionName")]
public string? SessionName
{
get;
set;
}
[JsonPropertyName("bossId")]
public int BossId
{
get;
set;
}
[JsonPropertyName("players")]
public List<string> Players
{
get;
set;
} = new();
[JsonPropertyName("status")]
public string? Status
{
get;
set;
}
[JsonPropertyName("tokens")]
public Dictionary<string, string> Tokens
{
get;
set;
} = new();
}
}

View File

@@ -16,6 +16,12 @@ public class Channel
// 채널 맵 관리
private readonly Dictionary<int, AMap> maps = new();
// 진행중 채널 맵 관리
private readonly Dictionary<int, AMap> useInstanceMaps = new();
// 동적 레이드 맵 할당용 ID 카운터 (사전 생성 10개 이후부터 시작)
private int nextDynamicRaidMapId = 1011;
// 파티
private readonly PartyManager partyManager = new();
@@ -29,7 +35,7 @@ public class Channel
maps.Add(1, new Robby(1));
// 인던
int defaultValue = 10;
int defaultValue = 1000;
for (int i = 1; i <= 10; i++)
{
maps.Add(i + defaultValue, new BossInstance(i + defaultValue));
@@ -152,9 +158,47 @@ public class Channel
return map;
}
// 사용 가능한 레이드 맵 ID 반환
// 기존 맵(1001~) 중 미사용 탐색 → 없으면 동적 생성 후 반환
public int GetOrCreateAvailableRaidMap()
{
// 기존 맵 중 미사용 탐색
foreach (int mapId in maps.Keys)
{
if (mapId >= 1001 && !useInstanceMaps.ContainsKey(mapId))
{
return mapId;
}
}
// 모두 사용 중 → 동적 생성
int newMapId = nextDynamicRaidMapId++;
BossInstance newMap = new(newMapId);
maps.Add(newMapId, newMap);
return newMapId;
}
// 레이드 맵 사용 시작 (진행중 목록에 등록)
public void AddInstanceMap(int mapId)
{
if (maps.TryGetValue(mapId, out AMap? map))
{
useInstanceMaps[mapId] = map;
}
}
// 레이드 맵 사용 종료 (진행중 목록에서 제거)
public void RemoveInstanceMap(int mapId)
{
useInstanceMaps.Remove(mapId);
}
// 파티매니저 가져옴
public PartyManager GetPartyManager()
{
return partyManager;
}
// TODO : 채널 가져오기
}

View File

@@ -30,6 +30,7 @@ public class GameServer : ServerBase
[(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
};
@@ -102,6 +103,7 @@ public class GameServer : ServerBase
{
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
string token = accTokenPacket.Token;
string username = "";
tokenHash.TryGetValue(token, out int hashKey);
if (hashKey <= 1000)
{
@@ -119,7 +121,7 @@ public class GameServer : ServerBase
else
{
// 신규 연결: 웹서버에 JWT 검증 요청
string? username = await RestApi.Instance.VerifyTokenAsync(token);
username = await RestApi.Instance.VerifyTokenAsync(token);
if (username == null)
{
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
@@ -133,6 +135,11 @@ public class GameServer : ServerBase
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);
@@ -451,7 +458,7 @@ public class GameServer : ServerBase
{
HashKey = hashKey,
PlayerId = hashKey,
Nickname = hashKey.ToString()
Nickname = ((Session)peer.Tag).UserName
};
// 채널에 추가
@@ -481,12 +488,16 @@ public class GameServer : ServerBase
{
foreach (var (userId, p) in initMap.GetUsers())
{
if (userId == hashKey) continue;
if (userId == hashKey)
{
continue;
}
response.Players.Add(ToPlayerInfo(p));
}
}
SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response));
SendTo(peer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response));
}
private void OnIntoChannelParty(NetPeer peer, int hashKey, byte[] payload)
@@ -1026,6 +1037,121 @@ public class GameServer : ServerBase
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<IntoBossRaidPacket>(new ReadOnlyMemory<byte>(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<string> userNames = new List<string>();
foreach (int memberId in party.PartyMemberIds)
{
userNames.Add(channel.GetPlayer(memberId).Nickname);
}
bool? result = await RestApi.Instance.BossRaidAccesssAsync(userNames, 1);
// 입장 실패
if (result.Value == false)
{
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);
}
// 레이드 맵 할당 (미사용 맵 탐색 → 없으면 동적 생성)
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));
}
// 파티장에게 입장 성공 응답 (할당된 실제 레이드 맵 ID 전달)
SendTo(peer,
PacketSerializer.Serialize((ushort)PacketCode.INTO_BOSS_RAID,
new IntoBossRaidPacket { RaidId = assignedRaidMapId, IsSuccess = true }));
Log.Debug("[GameServer] INTO_BOSS_RAID HashKey={Key} PartyId={PartyId} AssignedRaidMapId={RaidId}", hashKey, party.PartyId,
assignedRaidMapId);
}
// 채널 퇴장/연결 해제 시 파티 자동 탈퇴 처리
private void HandlePartyLeaveOnExit(int channelId, int hashKey)
{

View File

@@ -81,4 +81,11 @@ public class Player
get;
set;
}
// 레이드 입장 전 이전 맵 ID (레이드 종료 후 복귀용, 서버 캐싱)
public int PreviousMapId
{
get;
set;
}
}

View File

@@ -747,6 +747,30 @@ public class ChangeMapPacket
}
}
// INTO_BOSS_RAID
// 클라->서버: RaidId
// 서버->클라: RaidId + IsSuccess (파티장에게 결과 전달)
// 성공 시 파티원 전체에게 CHANGE_MAP 추가 전송
[ProtoContract]
public class IntoBossRaidPacket
{
// 입장할 보스 레이드 맵 Id
[ProtoMember(1)]
public int RaidId
{
get;
set;
}
// 입장 성공 여부 (서버 -> 클라)
[ProtoMember(2)]
public bool IsSuccess
{
get;
set;
}
}
// PARTY_CHANGE_MAP (클라 -> 서버 전용)
[ProtoContract]
public class PartyChangeMapPacket

View File

@@ -36,6 +36,9 @@ public enum PacketCode : ushort
// 단체로 맵 이동
PARTY_CHANGE_MAP,
// 파티장이 보스 레이드(인스턴스 던전) 입장 신청 (클라 -> 서버)
INTO_BOSS_RAID,
// 플레이어 위치, 방향 (서버 -> 클라 \ 클라 -> 서버)
TRANSFORM_PLAYER,

View File

@@ -16,25 +16,29 @@ public class Session
init;
}
public string UserName
{
get;
set;
}
public NetPeer Peer
{
get;
set;
}
// ─── 패킷 레이트 리미팅 ───────────────────────────
// 패킷 레이트 리미팅
private int packetCount;
private long windowStartTicks;
/// <summary>초당 허용 패킷 수</summary>
// 초당 허용 패킷 수
public int MaxPacketsPerSecond { get; set; }
/// <summary>연속 초과 횟수</summary>
// 연속 초과 횟수
public int RateLimitViolations { get; private set; }
/// <summary>
/// 패킷 수신 시 호출. 초당 제한 초과 시 true 반환.
/// </summary>
// 패킷 수신 시 호출. 초당 제한 초과 시 true 반환.
public bool CheckRateLimit()
{
long now = Environment.TickCount64;
@@ -57,7 +61,7 @@ public class Session
return false;
}
/// <summary>위반 카운트 초기화</summary>
// 위반 카운트 초기화
public void ResetViolations()
{
RateLimitViolations = 0;