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; using ProtoBuf; using Serilog; using ServerLib.Packet; using ServerLib.Service; namespace MMOserver.Game.Service; /* * 주요 게임서버 실행 관리 * - 세션 추가 / 삭제 관리 * - 패킷 핸들러 등록 * - Auth<->Web 인증 관리 * - 같은 Auth 재접속 가능 */ public partial class GameServer : ServerBase { private readonly Dictionary> packetHandlers; private readonly UuidGenerator userUuidGenerator; // 동일 토큰 동시 인증 방지 private readonly HashSet authenticatingTokens = new(); public GameServer(int port, string connectionString) : base(port, connectionString) { 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, [(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 }; userUuidGenerator = new UuidGenerator(); } protected override void HandleEcho(NetPeer peer, byte[] payload) { if (payload.Length < 4) { Log.Warning("[Server] Echo 페이로드 크기 오류 PeerId={Id} Length={Len}", peer.Id, payload.Length); return; } // 세션에 넣지는 않는다. NetDataReader reader = new(payload); short code = reader.GetShort(); short bodyLength = reader.GetShort(); EchoPacket echoPacket = Serializer.Deserialize(payload.AsMemory(4)); Log.Debug("[Echo] : addr={Addr}, str={Str}", peer.Address, echoPacket.Str); // Echo메시지는 순서보장 안함 HOL Blocking 제거 SendTo(peer, payload, DeliveryMethod.ReliableUnordered); } protected override void HandleAuthDummy(NetPeer peer, byte[] payload) { DummyAccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); int hashKey = accTokenPacket.Token; if (sessions.TryGetValue(hashKey, out NetPeer? existing)) { // WiFi → LTE 전환 등 재연결: 이전 피어 교체 existing.Tag = null; sessions.Remove(hashKey); Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id); existing.Disconnect(); } peer.Tag = new Session(hashKey, peer); sessions[hashKey] = peer; pendingPeers.Remove(peer.Id); if (hashKey <= 1000) { // 더미 클라다. ChannelManager cm = ChannelManager.Instance; Player newPlayer = new() { HashKey = hashKey, PlayerId = hashKey, Nickname = hashKey.ToString(), CurrentMapId = 1 }; cm.AddUser(1, hashKey, newPlayer, peer); } else { Log.Error("[Server] Dummy 클라이언트가 아닙니다. 연결을 종료합니다. HashKey={Key} PeerId={Id}", hashKey, peer.Id); peer.Disconnect(); return; } Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); OnSessionConnected(peer, hashKey); } protected override async Task HandleAuth(NetPeer peer, byte[] payload) { AccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); string token = accTokenPacket.Token; // 동일 토큰 동시 인증 방지 if (!authenticatingTokens.Add(token)) { Log.Warning("[Server] 동일 토큰 동시 인증 시도 차단 PeerId={Id}", peer.Id); peer.Disconnect(); return; } try { string? username = ""; int hashKey; bool isReconnect; lock (sessionLock) { isReconnect = tokenHash.TryGetValue(token, out hashKey) && hashKey > 1000; if (!isReconnect) { hashKey = userUuidGenerator.Create(); } if (isReconnect && sessions.TryGetValue(hashKey, out NetPeer? existing)) { // WiFi → LTE 전환 등 재연결: 이전 피어 교체 (토큰 재검증 불필요) existing.Tag = null; sessions.Remove(hashKey); Log.Information("[Server] 재연결 HashKey={Key} Old={Old} New={New}", hashKey, existing.Id, peer.Id); existing.Disconnect(); } } if (!isReconnect) { // 신규 연결: 웹서버에 JWT 검증 요청 username = await RestApi.Instance.VerifyTokenAsync(token); if (username == null) { Log.Warning("[Server] 토큰 검증 실패 - 연결 거부 PeerId={Id}", peer.Id); userUuidGenerator.Release(hashKey); peer.Disconnect(); return; } Log.Information("[Server] 토큰 검증 성공 Username={Username} PeerId={Id}", username, peer.Id); } // await 이후 — 공유 자원 접근 보호 lock (sessionLock) { 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); } Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); OnSessionConnected(peer, hashKey); } finally { authenticatingTokens.Remove(token); } } protected override void OnSessionConnected(NetPeer peer, int hashKey) { Log.Information("[GameServer] 세션 연결 HashKey={Key} PeerId={Id}", hashKey, peer.Id); // 만약 wifi-lte 로 바꿔졌다 이때 이미 로비에 들어가 있다면 넘긴다. ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId >= 0) { // 재연결: Channel 내 peer 참조 갱신 후 채널 유저 목록 전송 cm.GetChannel(channelId).UpdatePeer(hashKey, peer); SendIntoChannelPacket(peer, hashKey); } else { // 모든 채널 정보 던진다 SendLoadChannelPacket(peer, hashKey); } } protected override void OnSessionDisconnected(NetPeer peer, int hashKey, DisconnectInfo info) { ChannelManager cm = ChannelManager.Instance; // 제거 전에 채널/플레이어 정보 저장 (브로드캐스트에 필요) int channelId = cm.HasUser(hashKey); Player? player = channelId >= 0 ? cm.GetChannel(channelId).GetPlayer(hashKey) : null; // 퇴장 시 위치/플레이타임 DB 저장 (fire-and-forget) if (player != null && peer.Tag is Session session && session.Username != null) { long playTimeDelta = 0; if (session.ChannelJoinedAt != default) { playTimeDelta = (long)(DateTime.UtcNow - session.ChannelJoinedAt).TotalSeconds; } _ = RestApi.Instance.SaveGameDataAsync( session.Username, player.PosX, player.PosY, player.PosZ, player.RotY, playTimeDelta > 0 ? playTimeDelta : null ); } if (cm.RemoveUser(hashKey)) { Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason); if (channelId >= 0 && player != null) { // 파티 자동 탈퇴 HandlePartyLeaveOnExit(channelId, hashKey); // 같은 채널 유저들에게 나갔다고 알림 SendExitChannelPacket(peer, hashKey, channelId, player); } } userUuidGenerator.Release(hashKey); } protected override void HandlePacket(NetPeer peer, int hashKey, ushort type, byte[] payload) { if (packetHandlers.TryGetValue(type, out Action? handler)) { try { handler(peer, hashKey, payload); } catch (Exception ex) { Log.Error(ex, "[GameServer] 패킷 처리 중 예외 Type={Type} HashKey={Key}", type, hashKey); } } else { Log.Warning("[GameServer] 알 수 없는 패킷 Type={Type}", type); } } // 보내는 패킷 private void SendLoadChannelPacket(NetPeer peer, int hashKey) { LoadChannelPacket loadChannelPacket = new(); foreach (Channel.Channel channel in ChannelManager.Instance.GetChannels().Values) { if (channel.ChannelId <= 0) { continue; } if (channel.ChannelId >= 1000) { continue; } 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); SendTo(peer, data); } // 나간 유저를 같은 채널 유저들에게 알림 (UPDATE_CHANNEL_USER IsAdd=false) private void SendExitChannelPacket(NetPeer peer, int hashKey, int channelId, Player player) { UpdateChannelUserPacket packet = new() { Players = player.ToPlayerInfo(), IsAdd = false }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, packet); // 이미 채널에서 제거된 후라 나간 본인에게는 전송되지 않음 BroadcastToChannel(channelId, data); } // 채널 입장 시 패킷 전송 // - 새 유저에게 : 기존 채널 유저 목록 (INTO_CHANNEL) // - 기존 유저들에게 : 새 유저 입장 알림 (UPDATE_CHANNEL_USER IsAdd=true) private void SendIntoChannelPacket(NetPeer peer, int hashKey) { ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } Channel.Channel channel = cm.GetChannel(channelId); Player? myPlayer = channel.GetPlayer(hashKey); // 1. 새 유저에게: 자신을 제외한 기존 채널 유저 목록 + 파티 목록 전송 IntoChannelPacket response = new() { ChannelId = channelId }; foreach (int userId in channel.GetConnectUsers()) { if (userId == hashKey) { continue; } Player? channelPlayer = channel.GetPlayer(userId); if (channelPlayer != null) { response.Players.Add(channelPlayer.ToPlayerInfo()); } } foreach (PartyInfo party in channel.GetPartyManager().GetAllParties()) { response.Parties.Add(new PartyInfoData { PartyId = party.PartyId, LeaderId = party.LeaderId, MemberPlayerIds = new List(party.PartyMemberIds), PartyName = party.PartyName }); } byte[] toNewUser = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, response); SendTo(peer, toNewUser); // 2. 기존 유저들에게: 새 유저 입장 알림 if (myPlayer != null) { UpdateChannelUserPacket notify = new() { Players = myPlayer.ToPlayerInfo(), IsAdd = true }; byte[] toOthers = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_CHANNEL_USER, notify); BroadcastToChannel(channelId, toOthers, peer); } } // 채널 입장 시 패킷 전송 // - 자신 유저에게 : 내 정보 (LOAD_GAME) private void SendLoadGame(NetPeer peer, int hashKey) { ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); Player? player = channelId >= 0 ? cm.GetChannel(channelId).GetPlayer(hashKey) : null; if (player == null) { Log.Warning("[GameServer] LOAD_GAME 플레이어 없음 HashKey={Key}", hashKey); byte[] denied = PacketSerializer.Serialize((ushort)PacketCode.LOAD_GAME, new LoadGamePacket { IsAccepted = false }); SendTo(peer, denied); return; } LoadGamePacket packet = new() { IsAccepted = true, Player = player.ToPlayerInfo(), MapId = player.CurrentMapId }; 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); } /* * 채널 브로드캐스트 헬퍼 */ // 특정 채널의 모든 유저에게 전송 (exclude 지정 시 해당 피어 제외) / Channel이 NetPeer를 직접 보유하므로 sessions 교차 조회 없음 private void BroadcastToChannel(int channelId, byte[] data, NetPeer? exclude = null, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { Channel.Channel channel = ChannelManager.Instance.GetChannel(channelId); foreach (NetPeer targetPeer in channel.GetConnectPeers()) { if (exclude != null && targetPeer.Id == exclude.Id) { continue; } SendTo(targetPeer, data, method); } } // 특정 맵의 유저들에게 전송 (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); } } // 채널 퇴장/연결 해제 시 파티 자동 탈퇴 처리 private void HandlePartyLeaveOnExit(int channelId, int hashKey) { PartyManager pm = ChannelManager.Instance.GetChannel(channelId).GetPartyManager(); int? partyId = pm.GetPartyByPlayer(hashKey)?.PartyId; if (partyId == null) { return; // 파티에 없으면 무시 } pm.LeaveParty(hashKey, out PartyInfo? remaining); // 남은 멤버 있음 → LEAVE, 0명(파티 해산됨) → DELETE UpdatePartyPacket notify = remaining != null && remaining.PartyMemberIds.Count > 0 ? new UpdatePartyPacket { PartyId = partyId.Value, Type = PartyUpdateType.LEAVE, LeaderId = remaining.LeaderId, PlayerId = hashKey } : new UpdatePartyPacket { PartyId = partyId.Value, Type = PartyUpdateType.DELETE, LeaderId = -1, PlayerId = hashKey }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToChannel(channelId, data); } private void BroadcastToUsers(IEnumerable userIds, byte[] data, DeliveryMethod method = DeliveryMethod.ReliableOrdered) { foreach (int userId in userIds) { if (sessions.TryGetValue(userId, out NetPeer? targetPeer)) { SendTo(targetPeer, data, method); } } } private void SendError(NetPeer peer, ErrorCode code) { ErrorPacket err = new() { Code = code }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ERROR, err); SendTo(peer, data); } }