using LiteNetLib; using LiteNetLib.Utils; using MMOserver.Api; using MMOserver.Game.Channel; using MMOserver.Game.Party; using MMOserver.Packet; using MMOserver.Utils; using ProtoBuf; using Serilog; using ServerLib.Packet; using ServerLib.Service; namespace MMOserver.Game; public class GameServer : ServerBase { private readonly Dictionary> packetHandlers; private UuidGenerator userUuidGenerator; public GameServer(int port, string connectionString) : base(port, connectionString) { packetHandlers = new Dictionary> { [(ushort)PacketCode.INTO_CHANNEL] = OnIntoChannel, [(ushort)PacketCode.EXIT_CHANNEL] = OnExitChannel, [(ushort)PacketCode.TRANSFORM_PLAYER] = OnTransformPlayer, [(ushort)PacketCode.ACTION_PLAYER] = OnActionPlayer, [(ushort)PacketCode.STATE_PLAYER] = OnStatePlayer, [(ushort)PacketCode.REQUEST_PARTY] = OnRequestParty, }; 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 NetDataReader(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 Player { HashKey = hashKey, PlayerId = hashKey, Nickname = hashKey.ToString() }; 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 void HandleAuth(NetPeer peer, byte[] payload) { AccTokenPacket accTokenPacket = Serializer.Deserialize(new ReadOnlyMemory(payload)); string token = accTokenPacket.Token; tokenHash.TryGetValue(token, out int hashKey); if (hashKey <= 1000) { hashKey = userUuidGenerator.Create(); } 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(); } else { // 신규 연결: 웹서버에 JWT 검증 요청 string? 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); } peer.Tag = new Session(hashKey, peer); ((Session)peer.Tag).Token = token; sessions[hashKey] = peer; tokenHash[token] = hashKey; pendingPeers.Remove(peer.Id); Log.Information("[Server] 인증 완료 HashKey={Key} PeerId={Id}", hashKey, peer.Id); OnSessionConnected(peer, hashKey); } 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; if (cm.RemoveUser(hashKey)) { Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason); // 같은 채널 유저들에게 나갔다고 알림 if (channelId >= 0 && player != null) { 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)) { handler(peer, hashKey, payload); } else { Log.Warning("[GameServer] 알 수 없는 패킷 Type={Type}", type); } } // ============================================================ // 보내는 패킷 // ============================================================ private void SendLoadChannelPacket(NetPeer peer, int hashKey) { LoadChannelPacket loadChannelPacket = new LoadChannelPacket(); foreach (Channel.Channel channel in ChannelManager.Instance.GetChannels()) { ChannelInfo info = new ChannelInfo(); info.ChannelId = channel.ChannelId; info.ChannelUserConut = 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 UpdateChannelUserPacket { Players = ToPlayerInfo(player), 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 IntoChannelPacket { ChannelId = channelId }; foreach (int userId in channel.GetConnectUsers()) { if (userId == hashKey) { continue; } Player? p = channel.GetPlayer(userId); if (p != null) { response.Players.Add(ToPlayerInfo(p)); } } byte[] toNewUser = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, response); SendTo(peer, toNewUser); // 2. 기존 유저들에게: 새 유저 입장 알림 if (myPlayer != null) { UpdateChannelUserPacket notify = new UpdateChannelUserPacket { Players = ToPlayerInfo(myPlayer), 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 LoadGamePacket { IsAccepted = true, Player = ToPlayerInfo(player), MaplId = channelId, }; 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); } } // ============================================================ // Player ↔ PlayerInfo 변환 (패킷 전송 시에만 사용) // ============================================================ private static PlayerInfo ToPlayerInfo(Player player) { return new PlayerInfo { PlayerId = player.PlayerId, Nickname = player.Nickname, Level = player.Level, Hp = player.Hp, MaxHp = player.MaxHp, Mp = player.Mp, MaxMp = player.MaxMp, Position = new Vector3 { X = player.PosX, Y = player.PosY, Z = player.PosZ }, RotY = player.RotY, }; } // ============================================================ // 패킷 핸들러 // ============================================================ private void OnIntoChannel(NetPeer peer, int hashKey, byte[] payload) { IntoChannelPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; Channel.Channel channel = cm.GetChannel(packet.ChannelId); // 최대 인원 체크 if (channel.UserCount >= channel.UserCountMax) { Log.Warning("[GameServer] INTO_CHANNEL 채널 인원 초과 HashKey={Key} ChannelId={ChannelId} UserCount={Count}/{Max}", hashKey, packet.ChannelId, channel.UserCount, channel.UserCountMax); byte[] full = PacketSerializer.Serialize((ushort)PacketCode.INTO_CHANNEL, new IntoChannelPacket { ChannelId = -1 }); SendTo(peer, full); return; } // TODO: 실제 서비스에서는 DB/세션에서 플레이어 정보 로드 필요 Player newPlayer = new Player { HashKey = hashKey, PlayerId = hashKey, Nickname = hashKey.ToString() }; cm.AddUser(packet.ChannelId, hashKey, newPlayer, peer); Log.Debug("[GameServer] INTO_CHANNEL HashKey={Key} ChannelId={ChannelId}", hashKey, packet.ChannelId); // 접속된 모든 유저 정보 전달 SendIntoChannelPacket(peer, hashKey); // 내 정보 전달 SendLoadGame(peer, hashKey); } private void OnExitChannel(NetPeer peer, int hashKey, byte[] payload) { ExitChannelPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; // 제거 전에 채널/플레이어 정보 저장 (브로드캐스트에 필요) int channelId = cm.HasUser(hashKey); Player? player = channelId >= 0 ? cm.GetChannel(channelId).GetPlayer(hashKey) : null; cm.RemoveUser(hashKey); Log.Debug("[GameServer] EXIT_CHANNEL HashKey={Key} PlayerId={PlayerId}", hashKey, packet.PlayerId); // 같은 채널 유저들에게 나갔다고 알림 if (channelId >= 0 && player != null) { SendExitChannelPacket(peer, hashKey, channelId, player); } } private void OnTransformPlayer(NetPeer peer, int hashKey, byte[] payload) { TransformPlayerPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } // 채널 내 플레이어 위치/방향 상태 갱신 Player? player = cm.GetChannel(channelId).GetPlayer(hashKey); if (player != null) { 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); BroadcastToChannel(channelId, data, peer, DeliveryMethod.Unreliable); } private void OnActionPlayer(NetPeer peer, int hashKey, byte[] payload) { ActionPlayerPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } // 같은 채널 유저들에게 행동 브로드캐스트 (나 제외) byte[] data = PacketSerializer.Serialize((ushort)PacketCode.ACTION_PLAYER, packet); BroadcastToChannel(channelId, data, peer); } private void OnStatePlayer(NetPeer peer, int hashKey, byte[] payload) { StatePlayerPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; int channelId = cm.HasUser(hashKey); if (channelId < 0) { return; } // 채널 내 플레이어 HP/MP 상태 갱신 Player? player = cm.GetChannel(channelId).GetPlayer(hashKey); if (player != null) { 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); BroadcastToChannel(channelId, data, peer); } private void OnRequestParty(NetPeer peer, int hashKey, byte[] payload) { RequestPartyPacket req = Serializer.Deserialize(new ReadOnlyMemory(payload)); PartyManager pm = PartyManager.Instance; switch (req.Type) { case PartyUpdateType.CREATE: { if (!pm.CreateParty(hashKey, req.PartyName, out PartyInfo? party)) { return; } UpdatePartyPacket notify = new UpdatePartyPacket { PartyId = party!.PartyId, Type = PartyUpdateType.CREATE, LeaderId = party.LeaderId, PlayerId = hashKey, PartyName = party.PartyName, }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); SendTo(peer, data); break; } case PartyUpdateType.JOIN: { if (!pm.JoinParty(hashKey, req.PartyId, out PartyInfo? party)) { return; } UpdatePartyPacket notify = new UpdatePartyPacket { PartyId = party!.PartyId, Type = PartyUpdateType.JOIN, LeaderId = party.LeaderId, PlayerId = hashKey, }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToUsers(party.PartyMemberIds, data); // 새 멤버 포함 전원 break; } case PartyUpdateType.LEAVE: { if (!pm.LeaveParty(hashKey, out PartyInfo? party)) { return; } UpdatePartyPacket notify = new UpdatePartyPacket { PartyId = req.PartyId, Type = PartyUpdateType.LEAVE, LeaderId = party?.LeaderId ?? 0, PlayerId = hashKey, }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); if (party != null) BroadcastToUsers(party.PartyMemberIds, data); // 남은 멤버들에게 SendTo(peer, data); // 탈퇴자 본인에게 break; } case PartyUpdateType.DELETE: { if (!pm.DeleteParty(hashKey, req.PartyId, out PartyInfo? party)) { return; } UpdatePartyPacket notify = new UpdatePartyPacket { PartyId = party!.PartyId, Type = PartyUpdateType.DELETE, LeaderId = party.LeaderId, }; byte[] data = PacketSerializer.Serialize((ushort)PacketCode.UPDATE_PARTY, notify); BroadcastToUsers(party.PartyMemberIds, data); // 전원 (리더 포함) break; } } } 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); } } } }