2 Commits

Author SHA1 Message Date
qornwh1
fb76f49ec0 feat : 보스 전용채널 제거, 채널 5개로 변경, config.json 정리 2026-03-17 10:33:08 +09:00
qornwh1
f8fa34edbc feat : 보스 응답 메시지 문제 수정 2026-03-17 09:15:41 +09:00
7 changed files with 77 additions and 91 deletions

View File

@@ -4,10 +4,39 @@ namespace MMOserver.Api;
// API 응답(BossRaidAccessResponse)을 직접 노출하지 않고 이걸로 매핑해서 반환 // API 응답(BossRaidAccessResponse)을 직접 노출하지 않고 이걸로 매핑해서 반환
public sealed class BossRaidResult public sealed class BossRaidResult
{ {
public int RoomId { get; init; } public int RoomId
public string SessionName { get; init; } = string.Empty; {
public int BossId { get; init; } get;
public List<string> Players { get; init; } = new(); init;
public string Status { get; init; } = string.Empty; }
public Dictionary<string, string> Tokens { get; init; } = new();
public string SessionName
{
get;
init;
} = string.Empty;
public int BossId
{
get;
init;
}
public List<string> Players
{
get;
init;
} = new();
public string Status
{
get;
init;
} = string.Empty;
public Dictionary<string, string> Tokens
{
get;
init;
} = new();
} }

View File

@@ -73,13 +73,13 @@ public class RestApi : Singleton<RestApi>
// 성공 시 BossRaidResult 반환, 실패/거절 시 null 반환 // 성공 시 BossRaidResult 반환, 실패/거절 시 null 반환
public async Task<BossRaidResult?> BossRaidAccesssAsync(List<string> userNames, int bossId) public async Task<BossRaidResult?> BossRaidAccesssAsync(List<string> userNames, int bossId)
{ {
string url = AppConfig.RestApi.BaseUrl + "/api/internal/bossraid/entry"; string url = AppConfig.RestApi.BaseUrl + AppConfig.RestApi.BossRaidAccess;
for (int attempt = 1; attempt <= MAX_RETRY; attempt++) for (int attempt = 1; attempt <= MAX_RETRY; attempt++)
{ {
try try
{ {
HttpResponseMessage response = await httpClient.PostAsJsonAsync(url, new { usernames = userNames, bossId }); HttpResponseMessage response = await httpClient.PostAsJsonAsync(url, new { usernames = userNames, bossId = bossId });
// 401: API 키 인증 실패 // 401: API 키 인증 실패
if (response.StatusCode == HttpStatusCode.Unauthorized) if (response.StatusCode == HttpStatusCode.Unauthorized)
@@ -88,20 +88,13 @@ public class RestApi : Singleton<RestApi>
return null; return null;
} }
// 400: 입장 조건 미충족 (레벨 부족 등) // 400: 입장 조건 미충족 (레벨 부족, 이미 진행중 등)
if (response.StatusCode == HttpStatusCode.BadRequest) if (response.StatusCode == HttpStatusCode.BadRequest)
{ {
Log.Warning("[RestApi] 보스 레이드 입장 거절 (400) BossId={BossId}", bossId); Log.Warning("[RestApi] 보스 레이드 입장 거절 (400) BossId={BossId}", bossId);
return null; return null;
} }
// 409: 이미 진행 중이거나 슬롯 충돌
if (response.StatusCode == HttpStatusCode.Conflict)
{
Log.Warning("[RestApi] 보스 레이드 충돌 (409) BossId={BossId} - 이미 진행 중이거나 슬롯 없음", bossId);
return null;
}
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
BossRaidAccessResponse? raw = await response.Content.ReadFromJsonAsync<BossRaidAccessResponse>(); BossRaidAccessResponse? raw = await response.Content.ReadFromJsonAsync<BossRaidAccessResponse>();
@@ -118,7 +111,7 @@ public class RestApi : Singleton<RestApi>
BossId = raw.BossId, BossId = raw.BossId,
Players = raw.Players, Players = raw.Players,
Status = raw.Status ?? string.Empty, Status = raw.Status ?? string.Empty,
Tokens = raw.Tokens ?? new() Tokens = raw.Tokens
}; };
} }
catch (Exception ex) when (attempt < MAX_RETRY) catch (Exception ex) when (attempt < MAX_RETRY)
@@ -173,10 +166,10 @@ public class RestApi : Singleton<RestApi>
} }
[JsonPropertyName("tokens")] [JsonPropertyName("tokens")]
public Dictionary<string, string>? Tokens public Dictionary<string, string> Tokens
{ {
get; get;
set; set;
} } = new();
} }
} }

View File

@@ -30,9 +30,15 @@ public sealed class ServerConfig
get; get;
} }
public int ChannelCount
{
get;
}
public ServerConfig(IConfigurationSection section) public ServerConfig(IConfigurationSection section)
{ {
Port = int.Parse(section["Port"] ?? throw new InvalidOperationException("Server:Port is required in config.json")); Port = int.Parse(section["Port"] ?? throw new InvalidOperationException("Server:Port is required in config.json"));
ChannelCount = int.Parse(section["ChannelCount"] ?? "1");
} }
} }
@@ -48,6 +54,11 @@ public sealed class RestApiConfig
get; get;
} }
public string BossRaidAccess
{
get;
}
public string ApiKey public string ApiKey
{ {
get; get;
@@ -57,6 +68,7 @@ public sealed class RestApiConfig
{ {
BaseUrl = section["BaseUrl"] ?? throw new InvalidOperationException("RestApi:BaseUrl is required in config.json"); BaseUrl = section["BaseUrl"] ?? throw new InvalidOperationException("RestApi:BaseUrl is required in config.json");
VerifyToken = section["VerifyToken"] ?? throw new InvalidOperationException("RestApi:BaseUrl is required in config.json"); VerifyToken = section["VerifyToken"] ?? throw new InvalidOperationException("RestApi:BaseUrl is required in config.json");
BossRaidAccess = section["BossRaidAccess"] ?? throw new InvalidOperationException("RestApi:BaseUrl is required in config.json");
ApiKey = section["ApiKey"] ?? throw new InvalidOperationException("RestApi:ApiKey is required in config.json"); ApiKey = section["ApiKey"] ?? throw new InvalidOperationException("RestApi:ApiKey is required in config.json");
} }
} }

View File

@@ -1,4 +1,5 @@
using LiteNetLib; using LiteNetLib;
using MMOserver.Config;
using MMOserver.Utils; using MMOserver.Utils;
namespace MMOserver.Game.Channel; namespace MMOserver.Game.Channel;
@@ -8,16 +9,12 @@ public class ChannelManager : Singleton<ChannelManager>
// 채널 관리 // 채널 관리
private Dictionary<int, Channel> channels = new Dictionary<int, 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>();
public ChannelManager() public ChannelManager()
{ {
Initializer(); Initializer(AppConfig.Server.ChannelCount);
} }
public void Initializer(int channelSize = 1) public void Initializer(int channelSize = 1)
@@ -26,13 +23,6 @@ public class ChannelManager : Singleton<ChannelManager>
{ {
channels.Add(i, 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));
}
} }
public Channel GetChannel(int channelId) public Channel GetChannel(int channelId)

View File

@@ -103,7 +103,7 @@ public class GameServer : ServerBase
{ {
AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload)); AccTokenPacket accTokenPacket = Serializer.Deserialize<AccTokenPacket>(new ReadOnlyMemory<byte>(payload));
string token = accTokenPacket.Token; string token = accTokenPacket.Token;
string username = ""; string? username = "";
tokenHash.TryGetValue(token, out int hashKey); tokenHash.TryGetValue(token, out int hashKey);
if (hashKey <= 1000) if (hashKey <= 1000)
{ {
@@ -122,7 +122,7 @@ public class GameServer : ServerBase
{ {
// 신규 연결: 웹서버에 JWT 검증 요청 // 신규 연결: 웹서버에 JWT 검증 요청
username = await RestApi.Instance.VerifyTokenAsync(token); username = await RestApi.Instance.VerifyTokenAsync(token);
if (username == null) if (username == null || username.Trim().Length <= 0)
{ {
Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id); Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id);
userUuidGenerator.Release(hashKey); userUuidGenerator.Release(hashKey);
@@ -406,12 +406,7 @@ public class GameServer : ServerBase
Mp = player.Mp, Mp = player.Mp,
MaxMp = player.MaxMp, MaxMp = player.MaxMp,
Position = new Position { X = player.PosX, Y = player.PosY, Z = player.PosZ }, Position = new Position { X = player.PosX, Y = player.PosY, Z = player.PosZ },
RotY = player.RotY, RotY = player.RotY
Experience = player.Experience,
NextExp = player.NextExp,
AttackPower = player.AttackPower,
AttackRange = player.AttackRange,
SprintMultiplier = player.SprintMultiplier
}; };
} }
@@ -579,17 +574,12 @@ public class GameServer : ServerBase
if (memberPeer != null) if (memberPeer != null)
{ {
// 세션에서 username 조회
string nickname = memberPeer.Tag is Session s && !string.IsNullOrEmpty(s.UserName)
? s.UserName
: memberId.ToString();
// 새 채널에 유저 추가 // 새 채널에 유저 추가
Player newPlayer = new() Player newPlayer = new()
{ {
HashKey = memberId, HashKey = memberId,
PlayerId = memberId, PlayerId = memberId,
Nickname = nickname Nickname = memberId.ToString()
}; };
cm.AddUser(packet.ChannelId, memberId, newPlayer, memberPeer); cm.AddUser(packet.ChannelId, memberId, newPlayer, memberPeer);
@@ -1083,16 +1073,14 @@ public class GameServer : ServerBase
List<string> userNames = new List<string>(); List<string> userNames = new List<string>();
foreach (int memberId in party.PartyMemberIds) foreach (int memberId in party.PartyMemberIds)
{ {
Player? member = channel.GetPlayer(memberId); Player? memberPlayer = channel.GetPlayer(memberId);
if (member == null) if (memberPlayer != null)
{ {
continue; userNames.Add(memberPlayer.Nickname);
} }
userNames.Add(member.Nickname);
} }
BossRaidResult? result = await RestApi.Instance.BossRaidAccesssAsync(userNames, packet.RaidId); BossRaidResult? result = await RestApi.Instance.BossRaidAccesssAsync(userNames, 1);
// 입장 실패 // 입장 실패
if (result == null || result.BossId <= 0) if (result == null || result.BossId <= 0)
@@ -1159,13 +1147,17 @@ public class GameServer : ServerBase
SendTo(memberPeer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response)); SendTo(memberPeer, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, response));
// 각 파티원에게 레이드 입장 정보 전달 (본인의 토큰 포함) // 모두에게 레이드로 이동 (할당된 실제 레이드 맵 ID 전달)
string? memberToken = null; if (result.Tokens.ContainsKey(memberPlayer.Nickname.Trim()))
result.Tokens?.TryGetValue(memberPlayer.Nickname, out memberToken); {
SendTo(memberPeer, SendTo(peer,
PacketSerializer.Serialize((ushort)PacketCode.INTO_BOSS_RAID, PacketSerializer.Serialize((ushort)PacketCode.INTO_BOSS_RAID,
new IntoBossRaidPacket new IntoBossRaidPacket
{ RaidId = assignedRaidMapId, IsSuccess = true, Session = result.SessionName, Token = memberToken })); {
RaidId = assignedRaidMapId, IsSuccess = true, Session = result.SessionName,
Token = result.Tokens[memberPlayer.Nickname.Trim()]
}));
}
} }
Log.Debug("[GameServer] INTO_BOSS_RAID HashKey={Key} PartyId={PartyId} AssignedRaidMapId={RaidId}", hashKey, party.PartyId, Log.Debug("[GameServer] INTO_BOSS_RAID HashKey={Key} PartyId={PartyId} AssignedRaidMapId={RaidId}", hashKey, party.PartyId,

View File

@@ -75,38 +75,6 @@ public class Player
set; set;
} }
// 경험치
public int Experience
{
get;
set;
}
public int NextExp
{
get;
set;
}
// 전투 스탯
public float AttackPower
{
get;
set;
}
public float AttackRange
{
get;
set;
}
public float SprintMultiplier
{
get;
set;
}
// 현재 위치한 맵 ID // 현재 위치한 맵 ID
public int CurrentMapId public int CurrentMapId
{ {

View File

@@ -1,10 +1,12 @@
{ {
"Server": { "Server": {
"Port": 9500 "Port": 9500,
"ChannelCount": 5
}, },
"RestApi": { "RestApi": {
"BaseUrl": "https://a301.api.tolelom.xyz", "BaseUrl": "https://a301.api.tolelom.xyz",
"VerifyToken": "/api/internal/auth/verify", "VerifyToken": "/api/internal/auth/verify",
"BossRaidAccess": "/api/internal/bossraid/entry",
"ApiKey": "017f15b28143fc67d2e5bed283c37d2da858b9f294990a5334238e055e3f5425" "ApiKey": "017f15b28143fc67d2e5bed283c37d2da858b9f294990a5334238e055e3f5425"
}, },
"Database": { "Database": {