diff --git a/MMOTestServer/MMOserver/Api/BossRaidResult.cs b/MMOTestServer/MMOserver/Api/BossRaidResult.cs new file mode 100644 index 0000000..1a0abc3 --- /dev/null +++ b/MMOTestServer/MMOserver/Api/BossRaidResult.cs @@ -0,0 +1,13 @@ +namespace MMOserver.Api; + +// RestApi.BossRaidAccesssAsync 반환용 도메인 모델 +// API 응답(BossRaidAccessResponse)을 직접 노출하지 않고 이걸로 매핑해서 반환 +public sealed class BossRaidResult +{ + public int RoomId { get; init; } + public string SessionName { get; init; } = string.Empty; + public int BossId { get; init; } + public List Players { get; init; } = new(); + public string Status { get; init; } = string.Empty; + public Dictionary? Tokens { get; init; } +} diff --git a/MMOTestServer/MMOserver/Api/RestApi.cs b/MMOTestServer/MMOserver/Api/RestApi.cs index 99e1f7a..98132c3 100644 --- a/MMOTestServer/MMOserver/Api/RestApi.cs +++ b/MMOTestServer/MMOserver/Api/RestApi.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Http.Json; using System.Text.Json.Serialization; +using MMOserver.Config; using MMOserver.Utils; using Serilog; @@ -8,8 +9,6 @@ namespace MMOserver.Api; public class RestApi : Singleton { - private const string VERIFY_URL = "https://a301.api.tolelom.xyz"; - private const string INTERNAL_API_KEY = "017f15b28143fc67d2e5bed283c37d2da858b9f294990a5334238e055e3f5425"; private readonly HttpClient httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(5) }; private const int MAX_RETRY = 3; @@ -17,7 +16,7 @@ public class RestApi : Singleton public RestApi() { - httpClient.DefaultRequestHeaders.Add("X-API-Key", INTERNAL_API_KEY); + httpClient.DefaultRequestHeaders.Add("X-API-Key", AppConfig.RestApi.ApiKey); } // 토큰 검증 - 성공 시 username 반환 @@ -25,7 +24,7 @@ public class RestApi : Singleton // 타임아웃/네트워크 오류 → 최대 MAX_RETRY회 재시도 후 null 반환 public async Task VerifyTokenAsync(string token) { - string url = VERIFY_URL + "/api/internal/auth/verify"; + string url = AppConfig.RestApi.BaseUrl + AppConfig.RestApi.VerifyToken; for (int attempt = 1; attempt <= MAX_RETRY; attempt++) { try @@ -36,7 +35,7 @@ public class RestApi : Singleton if (response.StatusCode == HttpStatusCode.Unauthorized) { Log.Warning("[RestApi] 토큰 인증 실패 (401)"); - return null; + return ""; } response.EnsureSuccessStatusCode(); @@ -57,7 +56,7 @@ public class RestApi : Singleton } } - return null; + return ""; } // 플레이어 프로필 조회 - 성공 시 PlayerProfileResponse 반환 @@ -131,4 +130,108 @@ public class RestApi : Singleton [JsonPropertyName("sprintMultiplier")] public double SprintMultiplier { get; set; } } + + // 레이드 채널 접속 여부 체크 + // 성공 시 BossRaidResult 반환, 실패/거절 시 null 반환 + public async Task BossRaidAccesssAsync(List 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 null; + } + + // 400: 입장 조건 미충족 (레벨 부족, 이미 진행중 등) + if (response.StatusCode == HttpStatusCode.BadRequest) + { + Log.Warning("[RestApi] 보스 레이드 입장 거절 (400) BossId={BossId}", bossId); + return null; + } + + response.EnsureSuccessStatusCode(); + + BossRaidAccessResponse? raw = await response.Content.ReadFromJsonAsync(); + if (raw == null) + { + return null; + } + + // API 응답 → 도메인 모델 매핑 + return new BossRaidResult + { + RoomId = raw.RoomId, + SessionName = raw.SessionName ?? string.Empty, + BossId = raw.BossId, + Players = raw.Players, + Status = raw.Status ?? string.Empty, + Tokens = raw.Tokens + }; + } + 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 null; + } + + 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 Players + { + get; + set; + } = new(); + + [JsonPropertyName("status")] + public string? Status + { + get; + set; + } + + [JsonPropertyName("tokens")] + public Dictionary? Tokens + { + get; + set; + } + } } diff --git a/MMOTestServer/MMOserver/Config/AppConfig.cs b/MMOTestServer/MMOserver/Config/AppConfig.cs new file mode 100644 index 0000000..f93fad7 --- /dev/null +++ b/MMOTestServer/MMOserver/Config/AppConfig.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Configuration; + +namespace MMOserver.Config; + +public static class AppConfig +{ + public static ServerConfig Server + { + get; + private set; + } = null!; + + public static RestApiConfig RestApi + { + get; + private set; + } = null!; + + public static void Initialize(IConfiguration config) + { + Server = new ServerConfig(config.GetSection("Server")); + RestApi = new RestApiConfig(config.GetSection("RestApi")); + } +} + +public sealed class ServerConfig +{ + public int Port + { + get; + } + + public ServerConfig(IConfigurationSection section) + { + Port = int.Parse(section["Port"] ?? throw new InvalidOperationException("Server:Port is required in config.json")); + } +} + +public sealed class RestApiConfig +{ + public string BaseUrl + { + get; + } + + public string VerifyToken + { + get; + } + + public string ApiKey + { + get; + } + + public RestApiConfig(IConfigurationSection section) + { + 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"); + ApiKey = section["ApiKey"] ?? throw new InvalidOperationException("RestApi:ApiKey is required in config.json"); + } +} diff --git a/MMOTestServer/MMOserver/Game/Channel/Channel.cs b/MMOTestServer/MMOserver/Game/Channel/Channel.cs index cc6996f..98ee1ff 100644 --- a/MMOTestServer/MMOserver/Game/Channel/Channel.cs +++ b/MMOTestServer/MMOserver/Game/Channel/Channel.cs @@ -1,27 +1,51 @@ using LiteNetLib; using MMOserver.Game.Channel.Maps; +using MMOserver.Game.Channel.Maps.InstanceDungeun; using MMOserver.Game.Party; namespace MMOserver.Game.Channel; public class Channel { - // 로비 - private Robby robby = new Robby(); - - // 파티 - private PartyManager partyManager = new PartyManager(); + // 채널 내 유저 NetPeer (hashKey → NetPeer) — BroadcastToChannel 교차 조회 제거용 + private readonly Dictionary connectPeers = new(); // 채널 내 유저 상태 (hashKey → Player) - private Dictionary connectUsers = new Dictionary(); + private readonly Dictionary connectUsers = new(); - // 채널 내 유저 NetPeer (hashKey → NetPeer) — BroadcastToChannel 교차 조회 제거용 - private Dictionary connectPeers = new Dictionary(); + // 채널 맵 관리 + private readonly Dictionary maps = new(); + + // 진행중 채널 맵 관리 + private readonly Dictionary useInstanceMaps = new(); + + // 동적 레이드 맵 할당용 ID 카운터 (사전 생성 10개 이후부터 시작) + private int nextDynamicRaidMapId = 1011; + + // 파티 + private readonly PartyManager partyManager = new(); + + public Channel(int channelId) + { + ChannelId = channelId; + + // 일단 하드코딩으로 맵 생성 + { + // 로비 + maps.Add(1, new Robby(1)); + + // 인던 + int defaultValue = 1000; + for (int i = 1; i <= 10; i++) + { + maps.Add(i + defaultValue, new BossInstance(i + defaultValue)); + } + } + } public int ChannelId { get; - private set; } public int UserCount @@ -36,25 +60,49 @@ public class Channel private set; } = 100; - public Channel(int channelId) - { - ChannelId = channelId; - } - public void AddUser(int userId, Player player, NetPeer peer) { connectUsers[userId] = player; connectPeers[userId] = peer; UserCount++; + + // 처음 접속 시 1번 맵(로비)으로 입장 + ChangeMap(userId, player, 1); } public void RemoveUser(int userId) { + // 현재 맵에서도 제거 + if (connectUsers.TryGetValue(userId, out Player? player) && + maps.TryGetValue(player.CurrentMapId, out AMap? currentMap)) + { + currentMap.RemoveUser(userId); + } + connectUsers.Remove(userId); connectPeers.Remove(userId); UserCount--; } + // 맵 이동 (현재 맵 제거 → 새 맵 추가 → CurrentMapId 갱신) + public bool ChangeMap(int userId, Player player, int mapId) + { + if (!maps.TryGetValue(mapId, out AMap? newMap)) + { + return false; + } + + // 기존 맵에서 제거 + if (maps.TryGetValue(player.CurrentMapId, out AMap? oldMap)) + { + oldMap.RemoveUser(userId); + } + + newMap.AddUser(userId, player); + player.CurrentMapId = mapId; + return true; + } + // 재연결(WiFi→LTE 등) 시 동일 유저의 peer 교체 public void UpdatePeer(int userId, NetPeer peer) { @@ -96,10 +144,54 @@ public class Channel return -1; } - // 로비 가져옴 - public Robby GetRobby() + // 맵들 가져옴 + public Dictionary GetMaps() { - return robby; + return maps; + } + + // 맵 가져옴 + public AMap? GetMap(int mapId) + { + AMap? map = null; + maps.TryGetValue(mapId, out map); + 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); } // 파티매니저 가져옴 @@ -107,4 +199,6 @@ public class Channel { return partyManager; } + + // TODO : 채널 가져오기 } diff --git a/MMOTestServer/MMOserver/Game/Channel/Maps/AMap.cs b/MMOTestServer/MMOserver/Game/Channel/Maps/AMap.cs new file mode 100644 index 0000000..cda1f4d --- /dev/null +++ b/MMOTestServer/MMOserver/Game/Channel/Maps/AMap.cs @@ -0,0 +1,24 @@ +namespace MMOserver.Game.Channel.Maps; + +public abstract class AMap +{ + private Dictionary users = new Dictionary(); + + public abstract EnumMap GetMapType(); + public abstract int GetMapId(); + + public void AddUser(int userId, Player player) + { + users[userId] = player; + } + + public void RemoveUser(int userId) + { + users.Remove(userId); + } + + public Dictionary GetUsers() + { + return users; + } +} diff --git a/MMOTestServer/MMOserver/Game/Channel/Maps/EunmMap.cs b/MMOTestServer/MMOserver/Game/Channel/Maps/EunmMap.cs new file mode 100644 index 0000000..2380ed9 --- /dev/null +++ b/MMOTestServer/MMOserver/Game/Channel/Maps/EunmMap.cs @@ -0,0 +1,8 @@ +namespace MMOserver.Game.Channel.Maps; + +public enum EnumMap : int +{ + NONE = 0, + ROBBY = 1, + INSTANCE = 10, +} diff --git a/MMOTestServer/MMOserver/Game/Channel/Maps/InstanceDungeun/BossInstance.cs b/MMOTestServer/MMOserver/Game/Channel/Maps/InstanceDungeun/BossInstance.cs new file mode 100644 index 0000000..e9b6f9e --- /dev/null +++ b/MMOTestServer/MMOserver/Game/Channel/Maps/InstanceDungeun/BossInstance.cs @@ -0,0 +1,34 @@ +using MMOserver.Game.Channel.Maps; +using MMOserver.Game.Engine; + +namespace MMOserver.Game.Channel.Maps.InstanceDungeun; + +// 인스턴스 보스 맵에 들어갈때 쓰는 것 +public class BossInstance : AMap +{ + private EnumMap enumMap; + private int mapId; + + // 마을 시작 지점 넣어 둔다. + public static Vector3 StartPosition + { + get; + set; + } = new Vector3(0, 0, 0); + + public BossInstance(int mapId, EnumMap enumMap = EnumMap.INSTANCE) + { + this.enumMap = enumMap; + this.mapId = mapId; + } + + public override EnumMap GetMapType() + { + return enumMap; + } + + public override int GetMapId() + { + return mapId; + } +} diff --git a/MMOTestServer/MMOserver/Game/Channel/Maps/Robby.cs b/MMOTestServer/MMOserver/Game/Channel/Maps/Robby.cs index 14985d8..d8890c5 100644 --- a/MMOTestServer/MMOserver/Game/Channel/Maps/Robby.cs +++ b/MMOTestServer/MMOserver/Game/Channel/Maps/Robby.cs @@ -2,8 +2,11 @@ namespace MMOserver.Game.Channel.Maps; -public class Robby +public class Robby : AMap { + private EnumMap enumMap; + private int mapId; + // 마을 시작 지점 넣어 둔다. public static Vector3 StartPosition { @@ -11,7 +14,19 @@ public class Robby set; } = new Vector3(0, 0, 0); - public Robby() + public Robby(int mapId, EnumMap enumMap = EnumMap.ROBBY) { + this.enumMap = enumMap; + this.mapId = mapId; + } + + public override EnumMap GetMapType() + { + return enumMap; + } + + public override int GetMapId() + { + return mapId; } } diff --git a/MMOTestServer/MMOserver/Game/GameServer.cs b/MMOTestServer/MMOserver/Game/GameServer.cs index de25b12..4388dd1 100644 --- a/MMOTestServer/MMOserver/Game/GameServer.cs +++ b/MMOTestServer/MMOserver/Game/GameServer.cs @@ -2,6 +2,7 @@ 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; @@ -15,7 +16,7 @@ namespace MMOserver.Game; public class GameServer : ServerBase { private readonly Dictionary> packetHandlers; - private UuidGenerator userUuidGenerator; + private readonly UuidGenerator userUuidGenerator; public GameServer(int port, string connectionString) : base(port, connectionString) { @@ -27,8 +28,11 @@ public class GameServer : ServerBase [(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, + [(ushort)PacketCode.CHAT] = OnChat }; userUuidGenerator = new UuidGenerator(); } @@ -42,7 +46,7 @@ public class GameServer : ServerBase } // 세션에 넣지는 않는다. - NetDataReader reader = new NetDataReader(payload); + NetDataReader reader = new(payload); short code = reader.GetShort(); short bodyLength = reader.GetShort(); @@ -74,11 +78,12 @@ public class GameServer : ServerBase { // 더미 클라다. ChannelManager cm = ChannelManager.Instance; - Player newPlayer = new Player + Player newPlayer = new() { HashKey = hashKey, PlayerId = hashKey, - Nickname = hashKey.ToString() + Nickname = hashKey.ToString(), + CurrentMapId = 1 }; cm.AddUser(1, hashKey, newPlayer, peer); @@ -134,6 +139,7 @@ public class GameServer : ServerBase if (verifiedUsername != null) { ((Session)peer.Tag).Username = verifiedUsername; + ((Session)peer.Tag).UserName = verifiedUsername; } sessions[hashKey] = peer; tokenHash[token] = hashKey; @@ -214,7 +220,7 @@ public class GameServer : ServerBase private void SendLoadChannelPacket(NetPeer peer, int hashKey) { - LoadChannelPacket loadChannelPacket = new LoadChannelPacket(); + LoadChannelPacket loadChannelPacket = new(); foreach (Channel.Channel channel in ChannelManager.Instance.GetChannels().Values) { if (channel.ChannelId <= 0) @@ -227,26 +233,26 @@ public class GameServer : ServerBase continue; } - ChannelInfo info = new ChannelInfo(); + 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); + 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 UpdateChannelUserPacket + UpdateChannelUserPacket packet = new() { Players = ToPlayerInfo(player), IsAdd = false }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, packet); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, packet); // 이미 채널에서 제거된 후라 나간 본인에게는 전송되지 않음 BroadcastToChannel(channelId, data); } @@ -267,7 +273,7 @@ public class GameServer : ServerBase Player? myPlayer = channel.GetPlayer(hashKey); // 1. 새 유저에게: 자신을 제외한 기존 채널 유저 목록 + 파티 목록 전송 - IntoChannelPacket response = new IntoChannelPacket { ChannelId = channelId }; + IntoChannelPacket response = new() { ChannelId = channelId }; foreach (int userId in channel.GetConnectUsers()) { if (userId == hashKey) @@ -293,18 +299,18 @@ public class GameServer : ServerBase }); } - byte[] toNewUser = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, response); + byte[] toNewUser = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, response); SendTo(peer, toNewUser); // 2. 기존 유저들에게: 새 유저 입장 알림 if (myPlayer != null) { - UpdateChannelUserPacket notify = new UpdateChannelUserPacket + UpdateChannelUserPacket notify = new() { Players = ToPlayerInfo(myPlayer), IsAdd = true }; - byte[] toOthers = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, notify); + byte[] toOthers = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, notify); BroadcastToChannel(channelId, toOthers, peer); } } @@ -321,19 +327,19 @@ public class GameServer : ServerBase { Log.Warning("[GameServer] LOAD_GAME 플레이어 없음 HashKey={Key}", hashKey); byte[] denied = - PacketSerializer.Serialize((ushort)PacketCode.LOAD_GAME, new LoadGamePacket { IsAccepted = false }); + PacketSerializer.Serialize((ushort)PacketCode.LOAD_GAME, new LoadGamePacket { IsAccepted = false }); SendTo(peer, denied); return; } - LoadGamePacket packet = new LoadGamePacket + LoadGamePacket packet = new() { IsAccepted = true, Player = ToPlayerInfo(player), - MaplId = channelId, + MaplId = channelId }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.LOAD_GAME, packet); + 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); } @@ -359,6 +365,32 @@ public class GameServer : ServerBase } } + // 특정 맵의 유저들에게 전송 (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 변환 (패킷 전송 시에만 사용) // ============================================================ @@ -421,7 +453,7 @@ public class GameServer : ServerBase { 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, + byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, new IntoChannelPacket { ChannelId = -1 }); SendTo(peer, full); return; @@ -432,7 +464,7 @@ public class GameServer : ServerBase { HashKey = hashKey, PlayerId = hashKey, - Nickname = hashKey.ToString() + Nickname = ((Session)peer.Tag).UserName }; Session? session = peer.Tag as Session; @@ -479,6 +511,34 @@ public class GameServer : ServerBase // 내 정보 전달 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) @@ -500,15 +560,15 @@ public class GameServer : ServerBase } // 새로운 파티를 복사한다 - PartyInfo newParty = new PartyInfo(); - newParty.DeepCopySemi(preParty); + PartyInfo tempParty = new(); + tempParty.DeepCopySemi(preParty); // 최대 인원 체크 - if (newChannel.UserCount + newParty.PartyMemberIds.Count >= newChannel.UserCountMax) + 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, + byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, new IntoChannelPacket { ChannelId = -1 }); SendTo(peer, full); return; @@ -520,42 +580,43 @@ public class GameServer : ServerBase Player? player = preChannel.GetPlayer(memberId); if (player != null) { - UpdateChannelUserPacket exitNotify = new UpdateChannelUserPacket + UpdateChannelUserPacket exitNotify = new() { Players = ToPlayerInfo(player), IsAdd = false }; - byte[] exitData = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, exitNotify); + byte[] exitData = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, exitNotify); BroadcastToChannel(preChannelId, exitData); // 이전 채널에서 제거 preChannel.RemoveUser(memberId); // 현재 존재하는 파티원만 추가한다. - newParty.PartyMemberIds.Add(memberId); + tempParty.PartyMemberIds.Add(memberId); } } // 이전채널에서 파티를 지운다. preChannel.GetPartyManager().DeleteParty(hashKey, packet.PartyId, out preParty); - UpdatePartyPacket notify = new UpdatePartyPacket + UpdatePartyPacket notify = new() { PartyId = preParty!.PartyId, Type = PartyUpdateType.DELETE, - LeaderId = preParty.LeaderId, + LeaderId = preParty.LeaderId }; - BroadcastToChannel(preChannelId, - PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify)); // 채널 전체 파티 목록 갱신 + + // 채널 전체 파티 목록 갱신 + BroadcastToChannel(preChannelId, PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify)); // 새로운 채널에 파티원 넣기 - foreach (int memberId in newParty.PartyMemberIds) + foreach (int memberId in tempParty.PartyMemberIds) { sessions.TryGetValue(memberId, out NetPeer? memberPeer); if (memberPeer != null) { // 새 채널에 유저 추가 - Player newPlayer = new Player + Player newPlayer = new() { HashKey = memberId, PlayerId = memberId, @@ -572,17 +633,25 @@ public class GameServer : ServerBase } // 새로운 채널에 파티를 추가한다. - newChannel.GetPartyManager().CreateParty(newParty.LeaderId, newParty.PartyName, out newParty, newParty.PartyMemberIds); - - // 새 채널 기존 유저들에게 파티 생성 알림 - UpdatePartyPacket createNotify = new UpdatePartyPacket + if (newChannel.GetPartyManager() + .CreateParty(tempParty.LeaderId, tempParty.PartyName, out PartyInfo? createdParty, tempParty.PartyMemberIds) && + createdParty != null) { - PartyId = newParty.PartyId, - Type = PartyUpdateType.CREATE, - LeaderId = newParty.LeaderId, - PartyName = newParty.PartyName, - }; - BroadcastToChannel(packet.ChannelId, PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, createNotify)); + // 새 채널 기존 유저들에게 파티 생성 알림 + 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) @@ -624,17 +693,19 @@ public class GameServer : ServerBase // 채널 내 플레이어 위치/방향 상태 갱신 Player? player = cm.GetChannel(channelId).GetPlayer(hashKey); - if (player != null) + if (player == null) { - player.PosX = packet.Position.X; - player.PosY = packet.Position.Y; - player.PosZ = packet.Position.Z; - player.RotY = packet.RotY; + return; } - // 같은 채널 유저들에게 위치/방향 브로드캐스트 (나 제외) - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.TRANSFORM_PLAYER, packet); - BroadcastToChannel(channelId, data, peer, DeliveryMethod.Unreliable); + 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) @@ -648,9 +719,15 @@ public class GameServer : ServerBase return; } - // 같은 채널 유저들에게 행동 브로드캐스트 (나 제외) - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ACTION_PLAYER, packet); - BroadcastToChannel(channelId, data, peer); + 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) @@ -666,17 +743,19 @@ public class GameServer : ServerBase // 채널 내 플레이어 HP/MP 상태 갱신 Player? player = cm.GetChannel(channelId).GetPlayer(hashKey); - if (player != null) + if (player == null) { - player.Hp = packet.Hp; - player.MaxHp = packet.MaxHp; - player.Mp = packet.Mp; - player.MaxMp = packet.MaxMp; + return; } - // 같은 채널 유저들에게 스테이트 브로드캐스트 (나 제외) - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.STATE_PLAYER, packet); - BroadcastToChannel(channelId, data, peer); + 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) @@ -702,15 +781,15 @@ public class GameServer : ServerBase return; } - UpdatePartyPacket notify = new UpdatePartyPacket + UpdatePartyPacket notify = new() { PartyId = party!.PartyId, Type = PartyUpdateType.CREATE, LeaderId = party.LeaderId, PlayerId = hashKey, - PartyName = party.PartyName, + PartyName = party.PartyName }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } @@ -722,28 +801,28 @@ public class GameServer : ServerBase return; } - UpdatePartyPacket notify = new UpdatePartyPacket + UpdatePartyPacket notify = new() { PartyId = party!.PartyId, Type = PartyUpdateType.JOIN, LeaderId = party.LeaderId, - PlayerId = hashKey, + PlayerId = hashKey }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } case PartyUpdateType.LEAVE: { - if (!pm.LeaveParty(hashKey, out PartyInfo? party)) + if (!pm.LeaveParty(hashKey, out PartyInfo? party) || party == null) { SendError(peer, ErrorCode.PARTY_NOT_IN_PARTY); return; } - UpdatePartyPacket notify = new UpdatePartyPacket + UpdatePartyPacket notify = new() { - PartyId = party.PartyId, Type = PartyUpdateType.DELETE, LeaderId = party?.LeaderId ?? 0, PlayerId = hashKey, + PartyId = party.PartyId, Type = PartyUpdateType.DELETE, LeaderId = party.LeaderId, PlayerId = hashKey }; // 파티가 남아있으면 살린다. @@ -752,7 +831,7 @@ public class GameServer : ServerBase notify.Type = PartyUpdateType.LEAVE; } - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 (탈퇴자 포함) break; } @@ -764,13 +843,13 @@ public class GameServer : ServerBase return; } - UpdatePartyPacket notify = new UpdatePartyPacket + UpdatePartyPacket notify = new() { PartyId = party!.PartyId, Type = PartyUpdateType.DELETE, - LeaderId = party.LeaderId, + LeaderId = party.LeaderId }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } @@ -782,15 +861,15 @@ public class GameServer : ServerBase return; } - UpdatePartyPacket notify = new UpdatePartyPacket + UpdatePartyPacket notify = new() { PartyId = req.PartyId, Type = PartyUpdateType.UPDATE, LeaderId = party?.LeaderId ?? 0, PlayerId = hashKey, - PartyName = req.PartyName, + PartyName = req.PartyName }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); // 채널 전체 파티 목록 갱신 break; } @@ -815,16 +894,16 @@ public class GameServer : ServerBase } // 서버에서 발신자 정보 채워줌 (클라 위조 방지) - ChatPacket res = new ChatPacket + ChatPacket res = new() { Type = req.Type, SenderId = sender.PlayerId, SenderNickname = sender.Nickname, TargetId = req.TargetId, - Message = req.Message, + Message = req.Message }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.CHAT, res); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.CHAT, res); switch (req.Type) { @@ -866,6 +945,259 @@ public class GameServer : ServerBase } } + 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; + + if (!channel.ChangeMap(hashKey, player, packet.MapId)) + { + Log.Warning("[GameServer] CHANGE_MAP 유효하지 않은 맵 HashKey={Key} MapId={MapId}", hashKey, packet.MapId); + 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 = packet.MapId, IsAdd = true, Player = playerInfo }; + BroadcastToMap(channelId, packet.MapId, PacketSerializer.Serialize((ushort)PacketCode.CHANGE_MAP, enterNotify), peer); + + // 본인에게 새 맵 플레이어 목록 전달 + 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 == 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, packet.MapId); + } + + 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) + { + userNames.Add(channel.GetPlayer(memberId).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) { @@ -886,24 +1218,24 @@ public class GameServer : ServerBase PartyId = partyId.Value, Type = PartyUpdateType.LEAVE, LeaderId = remaining.LeaderId, - PlayerId = hashKey, + PlayerId = hashKey } : new UpdatePartyPacket { PartyId = partyId.Value, Type = PartyUpdateType.DELETE, LeaderId = -1, - PlayerId = hashKey, + PlayerId = hashKey }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); } private void SendError(NetPeer peer, ErrorCode code) { - ErrorPacket err = new ErrorPacket { Code = code }; - byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ERROR, err); + ErrorPacket err = new() { Code = code }; + byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ERROR, err); SendTo(peer, data); } diff --git a/MMOTestServer/MMOserver/Game/Party/PartyManager.cs b/MMOTestServer/MMOserver/Game/Party/PartyManager.cs index 857e020..7dca0b9 100644 --- a/MMOTestServer/MMOserver/Game/Party/PartyManager.cs +++ b/MMOTestServer/MMOserver/Game/Party/PartyManager.cs @@ -15,10 +15,9 @@ public class PartyManager // 파티 생성 public bool CreateParty(int leaderId, string partyName, out PartyInfo? party, List? memeberIds = null) { - party = null; - if (playerPartyMap.ContainsKey(leaderId)) { + party = null; return false; // 이미 파티에 속해있음 } diff --git a/MMOTestServer/MMOserver/Game/Player.cs b/MMOTestServer/MMOserver/Game/Player.cs index 6e1e4bf..da75369 100644 --- a/MMOTestServer/MMOserver/Game/Player.cs +++ b/MMOTestServer/MMOserver/Game/Player.cs @@ -104,4 +104,18 @@ public class Player get; set; } + + // 현재 위치한 맵 ID + public int CurrentMapId + { + get; + set; + } + + // 레이드 입장 전 이전 맵 ID (레이드 종료 후 복귀용, 서버 캐싱) + public int PreviousMapId + { + get; + set; + } } diff --git a/MMOTestServer/MMOserver/Packet/PacketBody.cs b/MMOTestServer/MMOserver/Packet/PacketBody.cs index a804474..e2ac3e9 100644 --- a/MMOTestServer/MMOserver/Packet/PacketBody.cs +++ b/MMOTestServer/MMOserver/Packet/PacketBody.cs @@ -278,9 +278,7 @@ public class PartyInfoData } } -// INTO_CHANNEL -// 클라->서버: 입장할 채널 ID -// 서버->클라: 채널 내 나 이외 플레이어 목록 +// INTO_CHANNEL 클라->서버: 입장할 채널 ID / 서버->클라: 채널 내 나 이외 플레이어 목록 [ProtoContract] public class IntoChannelPacket { @@ -744,6 +742,103 @@ public class ChatPacket set; } } +// ============================================================ +// 맵 이동 +// ============================================================ + +// CHANGE_MAP (클라 -> 서버 & 서버 -> 클라) +[ProtoContract] +public class ChangeMapPacket +{ + [ProtoMember(1)] + public int MapId + { + get; + set; + } + + // 새 맵의 기존 플레이어 목록 (이동한 본인에게 전달) + [ProtoMember(2)] + public List Players + { + get; + set; + } = new List(); + + // 입장(true) / 퇴장(false) - 기존 맵 플레이어들에게 전달 + [ProtoMember(3)] + public bool IsAdd + { + get; + set; + } + + // 이동한 플레이어 정보 - 기존 맵 플레이어들에게 전달 + [ProtoMember(4)] + public PlayerInfo Player + { + get; + set; + } +} + +// 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; + } + + [ProtoMember(3)] + public string Token + { + get; + set; + } + + [ProtoMember(4)] + public string Session + { + get; + set; + } +} + +// PARTY_CHANGE_MAP (클라 -> 서버 전용) +[ProtoContract] +public class PartyChangeMapPacket +{ + [ProtoMember(1)] + public int MapId + { + get; + set; + } + + [ProtoMember(2)] + public int PartyId + { + get; + set; + } +} + // ============================================================ // 파티 diff --git a/MMOTestServer/MMOserver/Packet/PacketHeader.cs b/MMOTestServer/MMOserver/Packet/PacketHeader.cs index 09b703d..44d012e 100644 --- a/MMOTestServer/MMOserver/Packet/PacketHeader.cs +++ b/MMOTestServer/MMOserver/Packet/PacketHeader.cs @@ -30,6 +30,15 @@ public enum PacketCode : ushort // 채널 나가기 (클라 -> 서버) EXIT_CHANNEL, + // 맵 이동 + CHANGE_MAP, + + // 단체로 맵 이동 + PARTY_CHANGE_MAP, + + // 파티장이 보스 레이드(인스턴스 던전) 입장 신청 (클라 -> 서버) + INTO_BOSS_RAID, + // 플레이어 위치, 방향 (서버 -> 클라 \ 클라 -> 서버) TRANSFORM_PLAYER, diff --git a/MMOTestServer/MMOserver/Program.cs b/MMOTestServer/MMOserver/Program.cs index cfc08b5..3145cde 100644 --- a/MMOTestServer/MMOserver/Program.cs +++ b/MMOTestServer/MMOserver/Program.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Configuration; +using MMOserver.Config; using MMOserver.Game; using MMOserver.RDB; using Serilog; @@ -21,6 +22,8 @@ class Program .AddEnvironmentVariables() // 도커 배포용 .Build(); + AppConfig.Initialize(config); + // DB 연결 // DbConnectionFactory dbFactory = new DbConnectionFactory(config); @@ -34,7 +37,7 @@ class Program Log.Information("Write Log Started"); - int port = 9500; + int port = AppConfig.Server.Port; string connectionString = "test"; GameServer gameServer = new GameServer(port, connectionString); diff --git a/MMOTestServer/MMOserver/config.json b/MMOTestServer/MMOserver/config.json index 9b09330..569a4d8 100644 --- a/MMOTestServer/MMOserver/config.json +++ b/MMOTestServer/MMOserver/config.json @@ -1,4 +1,12 @@ -{ +{ + "Server": { + "Port": 9500 + }, + "RestApi": { + "BaseUrl": "https://a301.api.tolelom.xyz", + "VerifyToken": "/api/internal/auth/verify", + "ApiKey": "017f15b28143fc67d2e5bed283c37d2da858b9f294990a5334238e055e3f5425" + }, "Database": { "Host": "localhost", "Port": "0000", diff --git a/MMOTestServer/ServerLib/Service/Session.cs b/MMOTestServer/ServerLib/Service/Session.cs index 0d5afd5..7ad9cec 100644 --- a/MMOTestServer/ServerLib/Service/Session.cs +++ b/MMOTestServer/ServerLib/Service/Session.cs @@ -22,25 +22,29 @@ public class Session init; } + public string UserName + { + get; + set; + } + public NetPeer Peer { get; set; } - // ─── 패킷 레이트 리미팅 ─────────────────────────── + // 패킷 레이트 리미팅 private int packetCount; private long windowStartTicks; - /// 초당 허용 패킷 수 + // 초당 허용 패킷 수 public int MaxPacketsPerSecond { get; set; } - /// 연속 초과 횟수 + // 연속 초과 횟수 public int RateLimitViolations { get; private set; } - /// - /// 패킷 수신 시 호출. 초당 제한 초과 시 true 반환. - /// + // 패킷 수신 시 호출. 초당 제한 초과 시 true 반환. public bool CheckRateLimit() { long now = Environment.TickCount64; @@ -63,7 +67,7 @@ public class Session return false; } - /// 위반 카운트 초기화 + // 위반 카운트 초기화 public void ResetViolations() { RateLimitViolations = 0;