using LiteNetLib; using MMOserver.Game.Channel; using MMOserver.Packet; 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 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); } } 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); } }