using LiteNetLib; using LiteNetLib.Utils; using MMOserver.Api; using MMOserver.Game.Channel; 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; 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, }; } 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)); long 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; cm.AddUser(1, hashKey, new Player()); } 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 long hashKey); if (hashKey <= 1000) { hashKey = UuidGeneratorManager.Instance.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); UuidGeneratorManager.Instance.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, long 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) { // 재연결: 채널 유저 목록 전송 (채널 선택 스킵, 바로 마을로) SendIntoChannelPacket(peer, hashKey); } else { // 모든 채널 정보 던진다 SendLoadChannelPacket(peer, hashKey); } } protected override void OnSessionDisconnected(NetPeer peer, long hashKey, DisconnectInfo info) { ChannelManager cm = ChannelManager.Instance; if (cm.RemoveUser(hashKey)) { Log.Information("[GameServer] 세션 해제 HashKey={Key} Reason={Reason}", hashKey, info.Reason); } UuidGeneratorManager.Instance.Release(hashKey); } protected override void HandlePacket(NetPeer peer, long 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, long 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, long 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, long 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 (long 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); } } // ============================================================ // 채널 브로드캐스트 헬퍼 // ============================================================ // 특정 채널의 모든 유저에게 전송 (exclude 지정 시 해당 피어 제외) private void BroadcastToChannel(int channelId, byte[] data, NetPeer? exclude = null) { Channel.Channel channel = ChannelManager.Instance.GetChannel(channelId); foreach (long userId in channel.GetConnectUsers()) { if (!sessions.TryGetValue(userId, out NetPeer? targetPeer)) { continue; } if (exclude != null && targetPeer.Id == exclude.Id) { continue; } SendTo(targetPeer, data); } } // ============================================================ // 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, long hashKey, byte[] payload) { IntoChannelPacket packet = Serializer.Deserialize(new ReadOnlyMemory(payload)); ChannelManager cm = ChannelManager.Instance; // TODO: 실제 서비스에서는 DB/세션에서 플레이어 정보 로드 필요 Player newPlayer = new Player { HashKey = hashKey, PlayerId = (int)(hashKey & 0x7FFFFFFF), }; cm.AddUser(packet.ChannelId, hashKey, newPlayer); Log.Debug("[GameServer] INTO_CHANNEL HashKey={Key} ChannelId={ChannelId}", hashKey, packet.ChannelId); // 접속된 모든 유저 정보 전달 SendIntoChannelPacket(peer, hashKey); } private void OnExitChannel(NetPeer peer, long 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, long 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); } private void OnActionPlayer(NetPeer peer, long 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, long 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); } }