From 88e88d3c4b84ae4b7059f1160059f9dd0aa6a3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sun, 27 Apr 2025 11:16:29 -0300 Subject: [PATCH 1/9] Fix LoginSession not disposing --- Maple2.Server.Core/Network/Session.cs | 8 ++ .../proto/channel/channel.proto | 2 + Maple2.Server.Core/proto/common.proto | 8 ++ Maple2.Server.Core/proto/sync.proto | 1 + Maple2.Server.Core/proto/world/world.proto | 3 + .../PacketHandlers/ResponseHeartbeat.cs | 17 +++ .../Service/ChannelService.Sync.cs | 22 ++- Maple2.Server.Game/Session/GameSession.cs | 3 + .../PacketHandlers/LoginHandler.cs | 67 +++++---- .../PacketHandlers/QuitHandler.cs | 14 ++ .../PacketHandlers/ResponseHeartbeat.cs | 17 +++ Maple2.Server.Login/Session/LoginSession.cs | 13 +- .../Containers/PlayerInfoLookup.cs | 6 +- .../Service/WorldService.Sync.cs | 134 ++++++++++++------ 14 files changed, 233 insertions(+), 82 deletions(-) create mode 100644 Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs create mode 100644 Maple2.Server.Login/PacketHandlers/QuitHandler.cs create mode 100644 Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs diff --git a/Maple2.Server.Core/Network/Session.cs b/Maple2.Server.Core/Network/Session.cs index 8906fbeef..3d62d1337 100644 --- a/Maple2.Server.Core/Network/Session.cs +++ b/Maple2.Server.Core/Network/Session.cs @@ -10,6 +10,7 @@ using Maple2.PacketLib.Crypto; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; +using Maple2.Server.Core.Packets; using Maple2.Tools.Extensions; using Serilog; @@ -109,6 +110,13 @@ public void Disconnect() { Dispose(); } + protected void Heartbeat() { + while (State == SessionState.Connected) { + Thread.Sleep(TimeSpan.FromMinutes(1)); + Send(RequestPacket.Heartbeat()); + } + } + public bool Connected() { if (disposed) { return false; diff --git a/Maple2.Server.Core/proto/channel/channel.proto b/Maple2.Server.Core/proto/channel/channel.proto index f7cdcdda2..032ef28c9 100644 --- a/Maple2.Server.Core/proto/channel/channel.proto +++ b/Maple2.Server.Core/proto/channel/channel.proto @@ -42,6 +42,8 @@ service Channel { rpc PlayerWarp(maple2.PlayerWarpRequest) returns (maple2.PlayerWarpResponse); // Admin rpc Admin(maple2.AdminRequest) returns (maple2.AdminResponse); + // Disconnect + rpc Disconnect(maple2.DisconnectRequest) returns (maple2.DisconnectResponse); } message GuildRequest { diff --git a/Maple2.Server.Core/proto/common.proto b/Maple2.Server.Core/proto/common.proto index 1d5c66730..9b9b34ac2 100644 --- a/Maple2.Server.Core/proto/common.proto +++ b/Maple2.Server.Core/proto/common.proto @@ -333,3 +333,11 @@ message AdminResponse { int32 error = 1; string message = 2; } + +message DisconnectRequest { + int64 character_id = 1; +} + +message DisconnectResponse { + bool success = 1; +} diff --git a/Maple2.Server.Core/proto/sync.proto b/Maple2.Server.Core/proto/sync.proto index 9a10fba47..03d8d0549 100644 --- a/Maple2.Server.Core/proto/sync.proto +++ b/Maple2.Server.Core/proto/sync.proto @@ -77,6 +77,7 @@ message PlayerUpdateResponse { message PlayerInfoRequest { int64 character_id = 1; + int64 account_id = 2; } message PlayerInfoResponse { diff --git a/Maple2.Server.Core/proto/world/world.proto b/Maple2.Server.Core/proto/world/world.proto index f12e29fc1..e100578d4 100644 --- a/Maple2.Server.Core/proto/world/world.proto +++ b/Maple2.Server.Core/proto/world/world.proto @@ -36,6 +36,7 @@ service World { rpc GroupChat(GroupChatRequest) returns (GroupChatResponse); // Retrieve player info from online player. rpc PlayerInfo(maple2.PlayerInfoRequest) returns (maple2.PlayerInfoResponse); + rpc AccountInfo(maple2.PlayerInfoRequest) returns (maple2.PlayerInfoResponse); // Update player info rpc UpdatePlayer(maple2.PlayerUpdateRequest) returns (maple2.PlayerUpdateResponse); // Notify character about new mail. @@ -56,6 +57,8 @@ service World { rpc Admin(maple2.AdminRequest) returns (maple2.AdminResponse); // Buff rpc PlayerConfig(PlayerConfigRequest) returns (PlayerConfigResponse); + // Disconnect + rpc Disconnect(maple2.DisconnectRequest) returns (maple2.DisconnectResponse); } enum Server { diff --git a/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs b/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs new file mode 100644 index 000000000..e3ac2c951 --- /dev/null +++ b/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs @@ -0,0 +1,17 @@ +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Game.Session; + +namespace Maple2.Server.Game.PacketHandlers; + +public class ResponseHeartbeat : PacketHandler { + public override RecvOp OpCode => RecvOp.ResponseHeartbeat; + + public override void Handle(GameSession session, IByteReader packet) { + int serverTick = packet.ReadInt(); + int clientTick = packet.ReadInt(); + + // should we do something with the ticks? + } +} diff --git a/Maple2.Server.Game/Service/ChannelService.Sync.cs b/Maple2.Server.Game/Service/ChannelService.Sync.cs index 3c3b7d83d..de1fb9723 100644 --- a/Maple2.Server.Game/Service/ChannelService.Sync.cs +++ b/Maple2.Server.Game/Service/ChannelService.Sync.cs @@ -1,4 +1,7 @@ using Grpc.Core; +using Maple2.Model.Game; +using Maple2.Server.Channel.Service; +using Maple2.Server.Core.Packets; using Maple2.Server.Game.Session; namespace Maple2.Server.Game.Service; @@ -11,11 +14,28 @@ public override Task MailNotification(MailNotification session.Mail.Notify(true); - return Task.FromResult(new MailNotificationResponse { Delivered = true }); + return Task.FromResult(new MailNotificationResponse { + Delivered = true, + }); } public override Task UpdatePlayer(PlayerUpdateRequest request, ServerCallContext context) { playerInfos.ReceiveUpdate(request); return Task.FromResult(new PlayerUpdateResponse()); } + + public override Task Disconnect(DisconnectRequest request, ServerCallContext context) { + if (request is { CharacterId: <= 0 }) { + throw new RpcException(new Status(StatusCode.InvalidArgument, $"CharacterId not specified")); + } + + if (!server.GetSession(request.CharacterId, out GameSession? session)) { + throw new RpcException(new Status(StatusCode.NotFound, $"Unable to find: {request.CharacterId}")); + } + + session.Disconnect(); + return Task.FromResult(new DisconnectResponse { + Success = true, + }); + } } diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index ebd0b5605..30cdd9aaa 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -40,6 +40,7 @@ public sealed partial class GameSession : Core.Network.Session { private bool disposed; private readonly GameServer server; + private readonly Thread heartbeatThread; public readonly CommandRouter CommandHandler; public readonly EventQueue Scheduler; @@ -115,6 +116,8 @@ public GameSession(TcpClient tcpClient, GameServer server, IComponentContext con OnLoop += Scheduler.InvokeAll; GroupChats = new ConcurrentDictionary(); Clubs = new ConcurrentDictionary(); + + heartbeatThread = new Thread(Heartbeat); } public bool FindSession(long characterId, [NotNullWhen(true)] out GameSession? other) { diff --git a/Maple2.Server.Login/PacketHandlers/LoginHandler.cs b/Maple2.Server.Login/PacketHandlers/LoginHandler.cs index 721207a9b..d2de849ff 100644 --- a/Maple2.Server.Login/PacketHandlers/LoginHandler.cs +++ b/Maple2.Server.Login/PacketHandlers/LoginHandler.cs @@ -30,36 +30,55 @@ private enum Command : byte { public override void Handle(LoginSession session, IByteReader packet) { var command = packet.Read(); + string user = packet.ReadUnicodeString(); + string pass = packet.ReadUnicodeString(); + packet.ReadShort(); // 1 + var machineId = packet.Read(); + + Logger.Debug("Logging in with user:{User}", user); + LoginResponse response = Global.Login(new LoginRequest { + Username = user, + Password = pass, + MachineId = machineId.ToString(), + }); + + if (response.Code != LoginResponse.Types.Code.Ok) { + session.Send(LoginResultPacket.Error((byte) response.Code, response.Message, response.AccountId)); + return; + } + + // Account is already logged into login server. + if (session.Server.GetSession(response.AccountId, out LoginSession? existing) && existing != session) { + existing.Disconnect(); + session.Send(LoginResultPacket.Error((byte) LoginResponse.Types.Code.AlreadyLogin, "", response.AccountId)); + return; + } + + PlayerInfoResponse? playerInfo = World.AccountInfo(new PlayerInfoRequest { + AccountId = response.AccountId, + }); + + if (playerInfo is not null && playerInfo.Channel > 0) { + DisconnectResponse? disconnectResponse = World.Disconnect(new DisconnectRequest { + CharacterId = playerInfo.CharacterId, + }); + if (disconnectResponse is null || !disconnectResponse.Success) { + Logger.Error("Failed to disconnect character: {CharacterId}", playerInfo.CharacterId); + } else { + Logger.Debug("Disconnected character: {CharacterId}", playerInfo.CharacterId); + } + + session.Send(LoginResultPacket.Error((byte) LoginResponse.Types.Code.AlreadyLogin, "", response.AccountId)); + return; + } + try { switch (command) { case Command.ServerList: session.ListServers(); + session.Disconnect(); return; case Command.CharacterList: - string user = packet.ReadUnicodeString(); - string pass = packet.ReadUnicodeString(); - packet.ReadShort(); // 1 - var machineId = packet.Read(); - - Logger.Debug("Logging in with user:{User}", user); - LoginResponse response = Global.Login(new LoginRequest { - Username = user, - Password = pass, - MachineId = machineId.ToString(), - }); - - if (response.Code != LoginResponse.Types.Code.Ok) { - session.Send(LoginResultPacket.Error((byte) response.Code, response.Message, response.AccountId)); - session.Disconnect(); - return; - } - - // TODO: Account is already logged into game server. - // if (World.PlayerInfo(new PlayerInfoRequest {AccountId = response.AccountId}).Location.Channel > 0) { - // session.Send(LoginResultPacket.Error((byte) LoginResponse.Types.Code.AlreadyLogin, "", response.AccountId)); - // return; - // } - session.Init(response.AccountId, machineId); session.Send(LoginResultPacket.Success(response.AccountId)); diff --git a/Maple2.Server.Login/PacketHandlers/QuitHandler.cs b/Maple2.Server.Login/PacketHandlers/QuitHandler.cs new file mode 100644 index 000000000..1468dd879 --- /dev/null +++ b/Maple2.Server.Login/PacketHandlers/QuitHandler.cs @@ -0,0 +1,14 @@ +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Login.Session; + +namespace Maple2.Server.Login.PacketHandlers; + +public class QuitHandler : PacketHandler { + public override RecvOp OpCode => RecvOp.RequestQuit; + + public override void Handle(LoginSession session, IByteReader packet) { + session.Disconnect(); + } +} diff --git a/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs b/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs new file mode 100644 index 000000000..2289f16af --- /dev/null +++ b/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs @@ -0,0 +1,17 @@ +using Maple2.PacketLib.Tools; +using Maple2.Server.Core.Constants; +using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Login.Session; + +namespace Maple2.Server.Login.PacketHandlers; + +public class ResponseHeartbeat : PacketHandler { + public override RecvOp OpCode => RecvOp.ResponseHeartbeat; + + public override void Handle(LoginSession session, IByteReader packet) { + int serverTick = packet.ReadInt(); + int clientTick = packet.ReadInt(); + + // should we do something with the ticks? + } +} diff --git a/Maple2.Server.Login/Session/LoginSession.cs b/Maple2.Server.Login/Session/LoginSession.cs index 620a8e2ed..f2b4f896d 100644 --- a/Maple2.Server.Login/Session/LoginSession.cs +++ b/Maple2.Server.Login/Session/LoginSession.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Net; using System.Net.Sockets; +using System.Threading; using Maple2.Database.Storage; using Maple2.Model.Enum; using Maple2.Model.Game; @@ -34,26 +35,21 @@ public class LoginSession : Core.Network.Session { #endregion private Account account = null!; + private readonly Thread heartbeatThread; public LoginSession(TcpClient tcpClient, LoginServer server) : base(tcpClient) { Server = server; State = SessionState.ChangeMap; + heartbeatThread = new Thread(Heartbeat); } public void Init(long accountId, Guid machineId) { AccountId = accountId; MachineId = machineId; - // Account is already logged into login server. - if (Server.GetSession(accountId, out LoginSession? existing) && existing != this) { - Send(LoginResultPacket.Error((byte) LoginResponse.Types.Code.AlreadyLogin, "", accountId)); - existing.Disconnect(); - Disconnect(); - return; - } - State = SessionState.Connected; Server.OnConnected(this); + heartbeatThread.Start(); } public void ListServers() { @@ -128,6 +124,7 @@ protected override void Dispose(bool disposing) { try { Server.OnDisconnected(this); State = SessionState.Disconnected; + heartbeatThread.Join(); Complete(); } finally { base.Dispose(disposing); diff --git a/Maple2.Server.World/Containers/PlayerInfoLookup.cs b/Maple2.Server.World/Containers/PlayerInfoLookup.cs index 8ecad9461..7dd15b333 100644 --- a/Maple2.Server.World/Containers/PlayerInfoLookup.cs +++ b/Maple2.Server.World/Containers/PlayerInfoLookup.cs @@ -7,6 +7,7 @@ using Grpc.Core; using Maple2.Database.Storage; using Maple2.Model.Game; +using Maple2.Server.Channel.Service; using Maple2.Server.Core.Sync; using Serilog; @@ -60,8 +61,9 @@ public bool TryGet(long characterId, [NotNullWhen(true)] out PlayerInfo? info) { // Try to get a cached player by account id and that's online, since we need a ChannelClient to notify. // If the player is not cached just return false since it was never online. // This is a very specific use case, and should be used with caution, mainly used for account wide mail notifications. - public PlayerInfo? TryGetByAccountId(long accountId) { - return cache.Values.FirstOrDefault(player => player.AccountId == accountId && player.Online); + public bool TryGetByAccountId(long accountId, [NotNullWhen(true)] out PlayerInfo? info) { + info = cache.Values.FirstOrDefault(player => player.AccountId == accountId && player.Online); + return info is not null; } public bool Update(PlayerUpdateRequest request) { diff --git a/Maple2.Server.World/Service/WorldService.Sync.cs b/Maple2.Server.World/Service/WorldService.Sync.cs index 29f05fb48..5acdb8db5 100644 --- a/Maple2.Server.World/Service/WorldService.Sync.cs +++ b/Maple2.Server.World/Service/WorldService.Sync.cs @@ -9,7 +9,7 @@ namespace Maple2.Server.World.Service; public partial class WorldService { public override Task PlayerInfo(PlayerInfoRequest request, ServerCallContext context) { - if (request.CharacterId <= 0) { + if (request is { CharacterId: <= 0 }) { throw new RpcException(new Status(StatusCode.InvalidArgument, $"AccountId and CharacterId not specified")); } @@ -17,52 +17,23 @@ public override Task PlayerInfo(PlayerInfoRequest request, S throw new RpcException(new Status(StatusCode.NotFound, $"Invalid character: {request.CharacterId}")); } - return Task.FromResult(new PlayerInfoResponse { - AccountId = info.AccountId, - CharacterId = info.CharacterId, - UpdateTime = info.UpdateTime, - Name = info.Name, - Motto = info.Motto, - Picture = info.Picture, - Gender = (int) info.Gender, - Job = (int) info.Job, - Level = info.Level, - GearScore = info.GearScore, - PremiumTime = info.PremiumTime, - LastOnlineTime = info.LastOnlineTime, - MapId = info.MapId, - Channel = info.Channel, - Health = new HealthUpdate { - CurrentHp = info.CurrentHp, - TotalHp = info.TotalHp, - }, - DeathState = (int) info.DeathState, - GuildName = info.GuildName, - GuildId = info.GuildId, - Home = new HomeUpdate { - Name = info.HomeName, - MapId = info.PlotMapId, - PlotNumber = info.PlotNumber, - ApartmentNumber = info.ApartmentNumber, - ExpiryTime = new Timestamp { - Seconds = info.PlotExpiryTime, - }, - }, - Trophy = new TrophyUpdate { - Combat = info.AchievementInfo.Combat, - Adventure = info.AchievementInfo.Adventure, - Lifestyle = info.AchievementInfo.Lifestyle, - }, - Clubs = { info.ClubIds.Select(id => new ClubUpdate { Id = id }) }, - DungeonEnterLimits = { info.DungeonEnterLimits.Select(dungeon => new DungeonEnterLimitUpdate { - DungeonId = dungeon.Key, - Limit = (int) dungeon.Value, - }) }, - }); + return Task.FromResult(PlayerInfoResponse(info)); + } + + public override Task AccountInfo(PlayerInfoRequest request, ServerCallContext context) { + if (request is { AccountId: <= 0 }) { + throw new RpcException(new Status(StatusCode.InvalidArgument, $"AccountId not specified")); + } + + if (!playerLookup.TryGetByAccountId(request.AccountId, out PlayerInfo? info)) { + return Task.FromResult(new PlayerInfoResponse()); + } + + return Task.FromResult(PlayerInfoResponse(info)); } public override Task UpdatePlayer(PlayerUpdateRequest request, ServerCallContext context) { - if (request.HasGender && request.Gender is not 0 or 1) { + if (request is { HasGender: true, Gender: not (0 or 1) }) { throw new RpcException(new Status(StatusCode.InvalidArgument, $"Player updated with invalid gender: {request.Gender}")); } if (request.HasJob && !Enum.IsDefined((Job) request.Job)) { @@ -75,13 +46,12 @@ public override Task UpdatePlayer(PlayerUpdateRequest requ } public override Task MailNotification(MailNotificationRequest request, ServerCallContext context) { - if (request.CharacterId <= 0 && request.AccountId <= 0) { + if (request is { CharacterId: <= 0, AccountId: <= 0 }) { throw new RpcException(new Status(StatusCode.InvalidArgument, "AccountId and CharacterId not specified")); } if (!playerLookup.TryGet(request.CharacterId, out PlayerInfo? info)) { - info = playerLookup.TryGetByAccountId(request.AccountId); - if (info == null) { + if (!playerLookup.TryGetByAccountId(request.AccountId, out info)) { return Task.FromResult(new MailNotificationResponse()); } } @@ -103,4 +73,74 @@ public override Task MailNotification(MailNotification return Task.FromResult(new MailNotificationResponse()); } } + + public override Task Disconnect(DisconnectRequest request, ServerCallContext context) { + if (request is { CharacterId: <= 0 }) { + throw new RpcException(new Status(StatusCode.InvalidArgument, $"CharacterId not specified")); + } + + if (!playerLookup.TryGet(request.CharacterId, out PlayerInfo? info) || info is { Online: false, Channel: <= 0 }) { + throw new RpcException(new Status(StatusCode.NotFound, $"Unable to find: {request.CharacterId}")); + } + + if (!channelClients.TryGetClient(info.Channel, out ChannelClient? channelClient)) { + logger.Error("No registry for channel: {Channel}", info.Channel); + return Task.FromResult(new DisconnectResponse()); + } + + return Task.FromResult(channelClient.Disconnect(new DisconnectRequest { + CharacterId = info.CharacterId, + })); + } + + private static PlayerInfoResponse PlayerInfoResponse(PlayerInfo info) { + return new PlayerInfoResponse { + AccountId = info.AccountId, + CharacterId = info.CharacterId, + UpdateTime = info.UpdateTime, + Name = info.Name, + Motto = info.Motto, + Picture = info.Picture, + Gender = (int) info.Gender, + Job = (int) info.Job, + Level = info.Level, + GearScore = info.GearScore, + PremiumTime = info.PremiumTime, + LastOnlineTime = info.LastOnlineTime, + MapId = info.MapId, + Channel = info.Channel, + Health = new HealthUpdate { + CurrentHp = info.CurrentHp, + TotalHp = info.TotalHp, + }, + DeathState = (int) info.DeathState, + GuildName = info.GuildName, + GuildId = info.GuildId, + Home = new HomeUpdate { + Name = info.HomeName, + MapId = info.PlotMapId, + PlotNumber = info.PlotNumber, + ApartmentNumber = info.ApartmentNumber, + ExpiryTime = new Timestamp { + Seconds = info.PlotExpiryTime, + }, + }, + Trophy = new TrophyUpdate { + Combat = info.AchievementInfo.Combat, + Adventure = info.AchievementInfo.Adventure, + Lifestyle = info.AchievementInfo.Lifestyle, + }, + Clubs = { + info.ClubIds.Select(id => new ClubUpdate { + Id = id, + }), + }, + DungeonEnterLimits = { + info.DungeonEnterLimits.Select(dungeon => new DungeonEnterLimitUpdate { + DungeonId = dungeon.Key, + Limit = (int) dungeon.Value, + }), + }, + }; + } } From 849f51857509bf3b527cb36e88d96b429e130559 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Sun, 27 Apr 2025 11:31:02 -0300 Subject: [PATCH 2/9] join heartbeat thread --- Maple2.Server.Game/Session/GameSession.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 30cdd9aaa..604e3abab 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -727,6 +727,7 @@ protected override void Dispose(bool disposing) { LeaveField(); Player.Value.Character.Channel = 0; Player.Value.Account.Online = false; + heartbeatThread.Join(); State = SessionState.Disconnected; Complete(); } finally { From 585525135f00e0f8c7d781a58a430340509db2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 28 Apr 2025 13:36:31 -0300 Subject: [PATCH 3/9] rework --- Maple2.Model/Metadata/Constants.cs | 2 + Maple2.Server.Core/Constants/Target.cs | 2 + Maple2.Server.Core/Network/Session.cs | 7 -- .../proto/channel/channel.proto | 2 + Maple2.Server.Core/proto/common.proto | 8 ++ Maple2.Server.Core/proto/login/login.proto | 10 ++ .../PacketHandlers/ResponseHeartbeat.cs | 27 ++++- .../Service/ChannelService.Heartbeat.cs | 21 ++++ .../Service/ChannelService.Sync.cs | 4 +- Maple2.Server.Game/Session/GameSession.cs | 9 +- .../PacketHandlers/ResponseHeartbeat.cs | 20 +++- Maple2.Server.Login/Program.cs | 100 ++++++++++++------ .../Service/LoginService.Heartbeat.cs | 15 +++ Maple2.Server.Login/Service/LoginService.cs | 14 +++ Maple2.Server.Login/Session/LoginSession.cs | 13 ++- .../Containers/PlayerInfoLookup.cs | 6 ++ Maple2.Server.World/Program.cs | 6 ++ Maple2.Server.World/WorldServer.cs | 44 +++++++- 18 files changed, 256 insertions(+), 54 deletions(-) create mode 100644 Maple2.Server.Core/proto/login/login.proto create mode 100644 Maple2.Server.Game/Service/ChannelService.Heartbeat.cs create mode 100644 Maple2.Server.Login/Service/LoginService.Heartbeat.cs create mode 100644 Maple2.Server.Login/Service/LoginService.cs diff --git a/Maple2.Model/Metadata/Constants.cs b/Maple2.Model/Metadata/Constants.cs index e82d360ab..84b3a0af8 100644 --- a/Maple2.Model/Metadata/Constants.cs +++ b/Maple2.Model/Metadata/Constants.cs @@ -113,6 +113,8 @@ public static class Constant { public const bool EnableRollEverywhere = false; public const bool HideHomeCommands = true; + public static int MaxAllowedLatency = TimeSpan.FromSeconds(2).Milliseconds; + public static IReadOnlyDictionary ContentRewards { get; } = new Dictionary { {"miniGame", 1005}, {"dungeonHelper", 1006}, diff --git a/Maple2.Server.Core/Constants/Target.cs b/Maple2.Server.Core/Constants/Target.cs index 313a83945..2b0597171 100644 --- a/Maple2.Server.Core/Constants/Target.cs +++ b/Maple2.Server.Core/Constants/Target.cs @@ -10,6 +10,8 @@ public static class Target { public static readonly IPAddress LoginIp = IPAddress.Loopback; public static readonly ushort LoginPort = 20001; + public static readonly ushort GrpcLoginPort = 21000; + public static readonly IPAddress GameIp = IPAddress.Loopback; public static readonly ushort BaseGamePort = 20002; diff --git a/Maple2.Server.Core/Network/Session.cs b/Maple2.Server.Core/Network/Session.cs index 3d62d1337..9d4dc85ce 100644 --- a/Maple2.Server.Core/Network/Session.cs +++ b/Maple2.Server.Core/Network/Session.cs @@ -110,13 +110,6 @@ public void Disconnect() { Dispose(); } - protected void Heartbeat() { - while (State == SessionState.Connected) { - Thread.Sleep(TimeSpan.FromMinutes(1)); - Send(RequestPacket.Heartbeat()); - } - } - public bool Connected() { if (disposed) { return false; diff --git a/Maple2.Server.Core/proto/channel/channel.proto b/Maple2.Server.Core/proto/channel/channel.proto index 032ef28c9..e60fa4614 100644 --- a/Maple2.Server.Core/proto/channel/channel.proto +++ b/Maple2.Server.Core/proto/channel/channel.proto @@ -44,6 +44,8 @@ service Channel { rpc Admin(maple2.AdminRequest) returns (maple2.AdminResponse); // Disconnect rpc Disconnect(maple2.DisconnectRequest) returns (maple2.DisconnectResponse); + // Heartbeat + rpc Heartbeat(maple2.HeartbeatRequest) returns (maple2.HeartbeatResponse); } message GuildRequest { diff --git a/Maple2.Server.Core/proto/common.proto b/Maple2.Server.Core/proto/common.proto index 9b9b34ac2..b63dafaf1 100644 --- a/Maple2.Server.Core/proto/common.proto +++ b/Maple2.Server.Core/proto/common.proto @@ -341,3 +341,11 @@ message DisconnectRequest { message DisconnectResponse { bool success = 1; } + +message HeartbeatRequest { + int64 character_id = 1; +} + +message HeartbeatResponse { + bool success = 1; +} diff --git a/Maple2.Server.Core/proto/login/login.proto b/Maple2.Server.Core/proto/login/login.proto new file mode 100644 index 000000000..618f2eda2 --- /dev/null +++ b/Maple2.Server.Core/proto/login/login.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package maple2.server.login.service; + +import "google/protobuf/empty.proto"; +import "common.proto"; + +service Login { + rpc Heartbeat(maple2.HeartbeatRequest) returns (maple2.HeartbeatResponse); +} diff --git a/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs b/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs index e3ac2c951..b4703f006 100644 --- a/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs +++ b/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs @@ -1,4 +1,5 @@ -using Maple2.PacketLib.Tools; +using Maple2.Model.Metadata; +using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; using Maple2.Server.Game.Session; @@ -12,6 +13,28 @@ public override void Handle(GameSession session, IByteReader packet) { int serverTick = packet.ReadInt(); int clientTick = packet.ReadInt(); - // should we do something with the ticks? + session.Latency = Environment.TickCount - serverTick; + if (session.Latency > Constant.MaxAllowedLatency) { +#if !DEBUG + session.Disconnect(); +#endif + return; + } + + if (serverTick == 0 || clientTick == 0) { + return; + } + + if (session is { LastClientTick: 0, LastServerTick: 0 }) { + session.LastClientTick = clientTick; + session.LastServerTick = serverTick; + return; + } + + int serverDelta = serverTick - session.LastServerTick; + int clientDelta = clientTick - session.LastClientTick; + + session.LastClientTick = clientTick; + session.LastServerTick = serverTick; } } diff --git a/Maple2.Server.Game/Service/ChannelService.Heartbeat.cs b/Maple2.Server.Game/Service/ChannelService.Heartbeat.cs new file mode 100644 index 000000000..7a2d8494b --- /dev/null +++ b/Maple2.Server.Game/Service/ChannelService.Heartbeat.cs @@ -0,0 +1,21 @@ +using Grpc.Core; +using Maple2.Server.Core.Packets; +using Maple2.Server.Game.Session; + +namespace Maple2.Server.Game.Service; + +public partial class ChannelService { + public override Task Heartbeat(HeartbeatRequest request, ServerCallContext context) { + if (request.CharacterId == 0) { + throw new RpcException(new Status(StatusCode.NotFound, "Character ID is 0.")); + } + if (!server.GetSession(request.CharacterId, out GameSession? session)) { + throw new RpcException(new Status(StatusCode.NotFound, "Session not found.")); + } + + session.Send(RequestPacket.Heartbeat()); + return Task.FromResult(new HeartbeatResponse { + Success = true, + }); + } +} diff --git a/Maple2.Server.Game/Service/ChannelService.Sync.cs b/Maple2.Server.Game/Service/ChannelService.Sync.cs index de1fb9723..11dd54034 100644 --- a/Maple2.Server.Game/Service/ChannelService.Sync.cs +++ b/Maple2.Server.Game/Service/ChannelService.Sync.cs @@ -30,7 +30,9 @@ public override Task Disconnect(DisconnectRequest request, S } if (!server.GetSession(request.CharacterId, out GameSession? session)) { - throw new RpcException(new Status(StatusCode.NotFound, $"Unable to find: {request.CharacterId}")); + return Task.FromResult(new DisconnectResponse { + Success = false, + }); } session.Disconnect(); diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 604e3abab..177398e46 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -40,11 +40,15 @@ public sealed partial class GameSession : Core.Network.Session { private bool disposed; private readonly GameServer server; - private readonly Thread heartbeatThread; public readonly CommandRouter CommandHandler; public readonly EventQueue Scheduler; + public int LastServerTick; + public int LastClientTick; + + public int Latency; + public long AccountId { get; private set; } public long CharacterId { get; private set; } public string PlayerName => Player.Value.Character.Name; @@ -116,8 +120,6 @@ public GameSession(TcpClient tcpClient, GameServer server, IComponentContext con OnLoop += Scheduler.InvokeAll; GroupChats = new ConcurrentDictionary(); Clubs = new ConcurrentDictionary(); - - heartbeatThread = new Thread(Heartbeat); } public bool FindSession(long characterId, [NotNullWhen(true)] out GameSession? other) { @@ -727,7 +729,6 @@ protected override void Dispose(bool disposing) { LeaveField(); Player.Value.Character.Channel = 0; Player.Value.Account.Online = false; - heartbeatThread.Join(); State = SessionState.Disconnected; Complete(); } finally { diff --git a/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs b/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs index 2289f16af..b7056f821 100644 --- a/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs +++ b/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs @@ -1,4 +1,5 @@ -using Maple2.PacketLib.Tools; +using System; +using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; using Maple2.Server.Core.PacketHandlers; using Maple2.Server.Login.Session; @@ -12,6 +13,21 @@ public override void Handle(LoginSession session, IByteReader packet) { int serverTick = packet.ReadInt(); int clientTick = packet.ReadInt(); - // should we do something with the ticks? + session.Latency = Environment.TickCount - serverTick; + + if (serverTick == 0 || clientTick == 0) { + return; + } + + if (session is { LastClientTick: 0, LastServerTick: 0 }) { + session.LastClientTick = clientTick; + session.LastServerTick = serverTick; + return; + } + + int serverDelta = serverTick - session.LastServerTick; + int clientDelta = clientTick - session.LastClientTick; + session.LastClientTick = clientTick; + session.LastServerTick = serverTick; } } diff --git a/Maple2.Server.Login/Program.cs b/Maple2.Server.Login/Program.cs index d09cbef90..07c1105a0 100644 --- a/Maple2.Server.Login/Program.cs +++ b/Maple2.Server.Login/Program.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System; +using System.Globalization; using System.IO; using Autofac; using Autofac.Extensions.DependencyInjection; @@ -8,12 +9,19 @@ using Microsoft.Extensions.Logging; using Serilog; using System.Reflection; +using Maple2.Database.Storage; +using Maple2.Server.Core.Constants; using Maple2.Server.Core.Modules; using Maple2.Server.Core.Network; using Maple2.Server.Core.PacketHandlers; +using Maple2.Server.Login.Service; using Maple2.Server.Login.Session; using Microsoft.Extensions.DependencyInjection; using Maple2.Tools; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Diagnostics.HealthChecks; // Force Globalization to en-US because we use periods instead of commas for decimals CultureInfo.CurrentCulture = new("en-US"); @@ -28,34 +36,62 @@ .ReadFrom.Configuration(configRoot) .CreateLogger(); -await Host.CreateDefaultBuilder() - .ConfigureLogging(logging => { - logging.ClearProviders(); - logging.AddSerilog(dispose: true); - }) - .ConfigureServices(services => { - services.RegisterModule(); - services.AddSingleton(); - services.AddHostedService(provider => provider.GetService()!); - }) - .UseServiceProviderFactory(new AutofacServiceProviderFactory()) - .ConfigureContainer(autofac => { - autofac.RegisterType>() - .As>() - .SingleInstance(); - autofac.RegisterType() - .PropertiesAutowired() - .AsSelf(); - - // Database - autofac.RegisterModule(); - autofac.RegisterModule(); - - // Make all packet handlers available to PacketRouter - autofac.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) - .Where(type => typeof(PacketHandler).IsAssignableFrom(type)) - .As>() - .PropertiesAutowired() - .SingleInstance(); - }) - .RunConsoleAsync(); +WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +builder.WebHost.UseKestrel(options => { + options.ListenAnyIP(Target.GrpcLoginPort, listen => { + listen.Protocols = HttpProtocols.Http2; + }); +}); + +builder.Logging.ClearProviders(); +builder.Logging.AddSerilog(dispose: true); + +builder.Services.AddGrpc(); +builder.Services.RegisterModule(); +builder.Services.AddMemoryCache(); + +builder.Services.AddSingleton(provider => new LoginServer( + provider.GetRequiredService>(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService() +)); +builder.Services.AddHostedService(provider => provider.GetService()!); + +builder.Services.AddGrpcHealthChecks(); +builder.Services.Configure(options => { + options.Delay = TimeSpan.Zero; + options.Period = TimeSpan.FromSeconds(10); +}); +builder.Services.AddHealthChecks() + .AddCheck("login_health_check"); + +builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); +builder.Host.ConfigureContainer(autofac => { + // Database + autofac.RegisterModule(); + autofac.RegisterModule(); + + autofac.RegisterType>() + .As>() + .SingleInstance(); + + autofac.RegisterType() + .PropertiesAutowired() + .AsSelf(); + + // Make all packet handlers available to PacketRouter + autofac.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) + .Where(type => typeof(PacketHandler).IsAssignableFrom(type)) + .As>() + .PropertiesAutowired() + .SingleInstance(); +}); + + +WebApplication app = builder.Build(); +app.UseRouting(); +app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client."); +app.MapGrpcService(); + +await app.RunAsync(); diff --git a/Maple2.Server.Login/Service/LoginService.Heartbeat.cs b/Maple2.Server.Login/Service/LoginService.Heartbeat.cs new file mode 100644 index 000000000..1979c64c5 --- /dev/null +++ b/Maple2.Server.Login/Service/LoginService.Heartbeat.cs @@ -0,0 +1,15 @@ +using System.Threading.Tasks; +using Grpc.Core; +using Maple2.Server.Core.Packets; +using Maple2.Server.Login.Session; + +namespace Maple2.Server.Login.Service; + +public partial class LoginService { + public override Task Heartbeat(HeartbeatRequest request, ServerCallContext context) { + foreach (LoginSession loginSession in loginServer.GetSessions()) { + loginSession.Send(RequestPacket.Heartbeat()); + } + return Task.FromResult(new HeartbeatResponse()); + } +} diff --git a/Maple2.Server.Login/Service/LoginService.cs b/Maple2.Server.Login/Service/LoginService.cs new file mode 100644 index 000000000..af93a8e17 --- /dev/null +++ b/Maple2.Server.Login/Service/LoginService.cs @@ -0,0 +1,14 @@ +using Grpc.Core; +using Serilog; + +namespace Maple2.Server.Login.Service; + +public partial class LoginService : Login.LoginBase { + private readonly LoginServer loginServer; + private readonly ILogger logger; + + public LoginService(LoginServer loginServer) { + this.loginServer = loginServer; + logger = Log.Logger.ForContext(); + } +} diff --git a/Maple2.Server.Login/Session/LoginSession.cs b/Maple2.Server.Login/Session/LoginSession.cs index f2b4f896d..ff19f3242 100644 --- a/Maple2.Server.Login/Session/LoginSession.cs +++ b/Maple2.Server.Login/Session/LoginSession.cs @@ -35,12 +35,15 @@ public class LoginSession : Core.Network.Session { #endregion private Account account = null!; - private readonly Thread heartbeatThread; + + public int LastServerTick; + public int LastClientTick; + + public int Latency; public LoginSession(TcpClient tcpClient, LoginServer server) : base(tcpClient) { Server = server; State = SessionState.ChangeMap; - heartbeatThread = new Thread(Heartbeat); } public void Init(long accountId, Guid machineId) { @@ -49,7 +52,6 @@ public void Init(long accountId, Guid machineId) { State = SessionState.Connected; Server.OnConnected(this); - heartbeatThread.Start(); } public void ListServers() { @@ -111,7 +113,9 @@ public void CreateCharacter(Character createCharacter, List createOutfits) Send(CharacterListPacket.SetMax(account.MaxCharacters, Constant.ServerMaxCharacters)); Send(CharacterListPacket.AppendEntry(account, character, - new Dictionary> { { ItemGroup.Outfit, outfits } })); + new Dictionary> { + { ItemGroup.Outfit, outfits } + })); } #region Dispose @@ -124,7 +128,6 @@ protected override void Dispose(bool disposing) { try { Server.OnDisconnected(this); State = SessionState.Disconnected; - heartbeatThread.Join(); Complete(); } finally { base.Dispose(disposing); diff --git a/Maple2.Server.World/Containers/PlayerInfoLookup.cs b/Maple2.Server.World/Containers/PlayerInfoLookup.cs index 7dd15b333..8e22599a8 100644 --- a/Maple2.Server.World/Containers/PlayerInfoLookup.cs +++ b/Maple2.Server.World/Containers/PlayerInfoLookup.cs @@ -144,4 +144,10 @@ private record Notification(PlayerInfo Info) { TryGet(id, out PlayerInfo? info); return info; } + + public List GetOnlinePlayerInfos() { + return cache.Values + .Where(player => player.Online) + .ToList(); + } } diff --git a/Maple2.Server.World/Program.cs b/Maple2.Server.World/Program.cs index 7545d0afe..e0a0c5f1d 100644 --- a/Maple2.Server.World/Program.cs +++ b/Maple2.Server.World/Program.cs @@ -6,6 +6,7 @@ using Maple2.Server.Core.Constants; using Maple2.Server.Core.Modules; using Maple2.Server.Global.Service; +using Maple2.Server.Login.Service; using Maple2.Server.World; using Maple2.Server.World.Containers; using Maple2.Server.World.Service; @@ -45,6 +46,11 @@ builder.Services.AddGrpc(); builder.Services.AddMemoryCache(); +builder.Services.AddGrpcClient(options => { + string loginService = Environment.GetEnvironmentVariable("GRPC_LOGIN_IP") ?? IPAddress.Loopback.ToString(); + options.Address = new Uri($"http://{loginService}:{Target.GrpcLoginPort}"); +}); + builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory()); builder.Host.ConfigureContainer(autofac => { // Database diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index c23f2999b..45c5a7d1d 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -2,13 +2,17 @@ using Maple2.Database.Extensions; using Maple2.Database.Storage; using Maple2.Model.Enum; +using Maple2.Model.Game; using Maple2.Model.Game.Event; using Maple2.Model.Metadata; using Maple2.Server.Channel.Service; +using Maple2.Server.Core.Sync; using Maple2.Server.World.Containers; using Maple2.Tools.Scheduler; using Serilog; using ChannelClient = Maple2.Server.Channel.Service.Channel.ChannelClient; +using LoginClient = Maple2.Server.Login.Service.Login.LoginClient; + namespace Maple2.Server.World; @@ -17,7 +21,9 @@ public class WorldServer { private readonly ChannelClientLookup channelClients; private readonly ServerTableMetadataStorage serverTableMetadata; private readonly GlobalPortalLookup globalPortalLookup; + private readonly PlayerInfoLookup playerInfoLookup; private readonly Thread thread; + private readonly Thread heartbeatThread; private readonly EventQueue scheduler; private readonly CancellationTokenSource tokenSource = new(); private readonly ConcurrentDictionary memoryStringBoards; @@ -25,11 +31,15 @@ public class WorldServer { private readonly ILogger logger = Log.ForContext(); - public WorldServer(GameStorage gameStorage, ChannelClientLookup channelClients, ServerTableMetadataStorage serverTableMetadata, GlobalPortalLookup globalPortalLookup) { + private readonly LoginClient login; + + public WorldServer(GameStorage gameStorage, ChannelClientLookup channelClients, ServerTableMetadataStorage serverTableMetadata, GlobalPortalLookup globalPortalLookup, PlayerInfoLookup playerInfoLookup, LoginClient login) { this.gameStorage = gameStorage; this.channelClients = channelClients; this.serverTableMetadata = serverTableMetadata; this.globalPortalLookup = globalPortalLookup; + this.playerInfoLookup = playerInfoLookup; + this.login = login; scheduler = new EventQueue(); scheduler.Start(); memoryStringBoards = []; @@ -39,6 +49,38 @@ public WorldServer(GameStorage gameStorage, ChannelClientLookup channelClients, ScheduleGameEvents(); thread = new Thread(Loop); thread.Start(); + + heartbeatThread = new Thread(Heartbeat); + heartbeatThread.Start(); + } + + private void Heartbeat() { + while (!tokenSource.Token.IsCancellationRequested) { + try { + Thread.Sleep(TimeSpan.FromSeconds(30)); + login.Heartbeat(new HeartbeatRequest()); + foreach (PlayerInfo playerInfo in playerInfoLookup.GetOnlinePlayerInfos()) { + if (playerInfo.CharacterId == 0) { + continue; + } + if (!channelClients.TryGetClient(playerInfo.Channel, out ChannelClient? channelClient)) { + continue; + } + + HeartbeatResponse? success = channelClient.Heartbeat(new HeartbeatRequest { + CharacterId = playerInfo.CharacterId, + }); + + if (success != null) continue; + + playerInfo.Channel = 0; + } + } catch (ThreadInterruptedException) { + break; + } catch { + // Ignore exceptions + } + } } private void Loop() { From 73499ffcdf06088ca769b6f3b49c5a3b56fbac92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 28 Apr 2025 14:05:52 -0300 Subject: [PATCH 4/9] oops --- Maple2.Server.Login/LoginServer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Maple2.Server.Login/LoginServer.cs b/Maple2.Server.Login/LoginServer.cs index 10ebfa7a5..b433b5492 100644 --- a/Maple2.Server.Login/LoginServer.cs +++ b/Maple2.Server.Login/LoginServer.cs @@ -78,4 +78,6 @@ public override Task StopAsync(CancellationToken cancellationToken) { return base.StopAsync(cancellationToken); } + + public List GetSessions() => sessions.Values.ToList(); } From f487c187234a1570a60fe503e91c6ade25192b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 28 Apr 2025 14:11:42 -0300 Subject: [PATCH 5/9] fix --- Maple2.Model/Game/User/PlayerInfo.cs | 4 ++-- Maple2.Server.Game/Session/GameSession.cs | 4 ++-- Maple2.Server.World/WorldServer.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Maple2.Model/Game/User/PlayerInfo.cs b/Maple2.Model/Game/User/PlayerInfo.cs index 323b4240e..1fb9d1a7b 100644 --- a/Maple2.Model/Game/User/PlayerInfo.cs +++ b/Maple2.Model/Game/User/PlayerInfo.cs @@ -126,7 +126,7 @@ public class CharacterInfo { public DeathState DeathState { get; set; } // Location public int MapId { get; set; } - public short Channel { get; set; } + public short Channel { get; set; } = -1; public long LastOnlineTime { get; set; } // Guild @@ -134,7 +134,7 @@ public class CharacterInfo { public string GuildName { get; set; } public long UpdateTime { get; set; } - public bool Online => Channel != 0; + public bool Online => Channel >= 0; public CharacterInfo(long accountId, long characterId, string name, string motto, string picture, Gender gender, Job job, short level) { AccountId = accountId; diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 177398e46..1653ae286 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -716,7 +716,7 @@ protected override void Dispose(bool disposing) { CharacterId = CharacterId, LastOnlineTime = DateTime.UtcNow.ToEpochSeconds(), MapId = 0, - Channel = 0, + Channel = -1, Async = true, }); @@ -727,7 +727,7 @@ protected override void Dispose(bool disposing) { Scheduler.Stop(); server.OnDisconnected(this); LeaveField(); - Player.Value.Character.Channel = 0; + Player.Value.Character.Channel = -1; Player.Value.Account.Online = false; State = SessionState.Disconnected; Complete(); diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index 45c5a7d1d..8ff44373c 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -73,7 +73,7 @@ private void Heartbeat() { if (success != null) continue; - playerInfo.Channel = 0; + playerInfo.Channel = -1; } } catch (ThreadInterruptedException) { break; From 77530555340450a3658e5e71e8f4eab45f16b0ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 28 Apr 2025 14:21:30 -0300 Subject: [PATCH 6/9] oops2 --- Maple2.Server.World/Service/WorldService.Sync.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maple2.Server.World/Service/WorldService.Sync.cs b/Maple2.Server.World/Service/WorldService.Sync.cs index 5acdb8db5..9f319f49e 100644 --- a/Maple2.Server.World/Service/WorldService.Sync.cs +++ b/Maple2.Server.World/Service/WorldService.Sync.cs @@ -79,7 +79,7 @@ public override Task Disconnect(DisconnectRequest request, S throw new RpcException(new Status(StatusCode.InvalidArgument, $"CharacterId not specified")); } - if (!playerLookup.TryGet(request.CharacterId, out PlayerInfo? info) || info is { Online: false, Channel: <= 0 }) { + if (!playerLookup.TryGet(request.CharacterId, out PlayerInfo? info) || info is { Online: false }) { throw new RpcException(new Status(StatusCode.NotFound, $"Unable to find: {request.CharacterId}")); } From be9f0cbb2f491fc2183e97044a3fdb2a204834e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 28 Apr 2025 14:32:14 -0300 Subject: [PATCH 7/9] suggestions --- Maple2.Server.Core/Network/Session.cs | 6 +----- ...artbeat.cs => ResponseHeartbeatHandler.cs} | 20 +++++++++---------- Maple2.Server.Game/Session/GameSession.cs | 4 ++-- ...artbeat.cs => ResponseHeartbeatHandler.cs} | 18 ++++++++--------- Maple2.Server.Login/Session/LoginSession.cs | 6 ++---- 5 files changed, 24 insertions(+), 30 deletions(-) rename Maple2.Server.Game/PacketHandlers/{ResponseHeartbeat.cs => ResponseHeartbeatHandler.cs} (53%) rename Maple2.Server.Login/PacketHandlers/{ResponseHeartbeat.cs => ResponseHeartbeatHandler.cs} (52%) diff --git a/Maple2.Server.Core/Network/Session.cs b/Maple2.Server.Core/Network/Session.cs index 9d4dc85ce..b25fb0686 100644 --- a/Maple2.Server.Core/Network/Session.cs +++ b/Maple2.Server.Core/Network/Session.cs @@ -1,16 +1,12 @@ -using System; -using System.Buffers; +using System.Buffers; using System.Collections.Concurrent; using System.IO.Pipelines; using System.Net.Sockets; using System.Security.Cryptography; -using System.Threading; -using System.Threading.Tasks; using Maple2.Model.Enum; using Maple2.PacketLib.Crypto; using Maple2.PacketLib.Tools; using Maple2.Server.Core.Constants; -using Maple2.Server.Core.Packets; using Maple2.Tools.Extensions; using Serilog; diff --git a/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs b/Maple2.Server.Game/PacketHandlers/ResponseHeartbeatHandler.cs similarity index 53% rename from Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs rename to Maple2.Server.Game/PacketHandlers/ResponseHeartbeatHandler.cs index b4703f006..c68d02f11 100644 --- a/Maple2.Server.Game/PacketHandlers/ResponseHeartbeat.cs +++ b/Maple2.Server.Game/PacketHandlers/ResponseHeartbeatHandler.cs @@ -6,15 +6,15 @@ namespace Maple2.Server.Game.PacketHandlers; -public class ResponseHeartbeat : PacketHandler { +public class ResponseHeartbeatHandler : PacketHandler { public override RecvOp OpCode => RecvOp.ResponseHeartbeat; public override void Handle(GameSession session, IByteReader packet) { int serverTick = packet.ReadInt(); int clientTick = packet.ReadInt(); - session.Latency = Environment.TickCount - serverTick; - if (session.Latency > Constant.MaxAllowedLatency) { + int latency = Environment.TickCount - serverTick; + if (latency > Constant.MaxAllowedLatency) { #if !DEBUG session.Disconnect(); #endif @@ -25,16 +25,16 @@ public override void Handle(GameSession session, IByteReader packet) { return; } - if (session is { LastClientTick: 0, LastServerTick: 0 }) { - session.LastClientTick = clientTick; - session.LastServerTick = serverTick; + if (session is { ClientTick: 0, ServerTick: 0 }) { + session.ClientTick = clientTick; + session.ServerTick = serverTick; return; } - int serverDelta = serverTick - session.LastServerTick; - int clientDelta = clientTick - session.LastClientTick; + int serverDelta = serverTick - session.ServerTick; + int clientDelta = clientTick - session.ClientTick; - session.LastClientTick = clientTick; - session.LastServerTick = serverTick; + session.ClientTick = clientTick; + session.ServerTick = serverTick; } } diff --git a/Maple2.Server.Game/Session/GameSession.cs b/Maple2.Server.Game/Session/GameSession.cs index 1653ae286..5fc9ef9d3 100644 --- a/Maple2.Server.Game/Session/GameSession.cs +++ b/Maple2.Server.Game/Session/GameSession.cs @@ -44,8 +44,8 @@ public sealed partial class GameSession : Core.Network.Session { public readonly CommandRouter CommandHandler; public readonly EventQueue Scheduler; - public int LastServerTick; - public int LastClientTick; + public int ServerTick; + public int ClientTick; public int Latency; diff --git a/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs b/Maple2.Server.Login/PacketHandlers/ResponseHeartbeatHandler.cs similarity index 52% rename from Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs rename to Maple2.Server.Login/PacketHandlers/ResponseHeartbeatHandler.cs index b7056f821..d25902708 100644 --- a/Maple2.Server.Login/PacketHandlers/ResponseHeartbeat.cs +++ b/Maple2.Server.Login/PacketHandlers/ResponseHeartbeatHandler.cs @@ -6,28 +6,28 @@ namespace Maple2.Server.Login.PacketHandlers; -public class ResponseHeartbeat : PacketHandler { +public class ResponseHeartbeatHandler : PacketHandler { public override RecvOp OpCode => RecvOp.ResponseHeartbeat; public override void Handle(LoginSession session, IByteReader packet) { int serverTick = packet.ReadInt(); int clientTick = packet.ReadInt(); - session.Latency = Environment.TickCount - serverTick; + if (serverTick == 0 || clientTick == 0) { return; } - if (session is { LastClientTick: 0, LastServerTick: 0 }) { - session.LastClientTick = clientTick; - session.LastServerTick = serverTick; + if (session is { ClientTick: 0, ServerTick: 0 }) { + session.ClientTick = clientTick; + session.ServerTick = serverTick; return; } - int serverDelta = serverTick - session.LastServerTick; - int clientDelta = clientTick - session.LastClientTick; - session.LastClientTick = clientTick; - session.LastServerTick = serverTick; + int serverDelta = serverTick - session.ServerTick; + int clientDelta = clientTick - session.ClientTick; + session.ClientTick = clientTick; + session.ServerTick = serverTick; } } diff --git a/Maple2.Server.Login/Session/LoginSession.cs b/Maple2.Server.Login/Session/LoginSession.cs index ff19f3242..58a67c9a6 100644 --- a/Maple2.Server.Login/Session/LoginSession.cs +++ b/Maple2.Server.Login/Session/LoginSession.cs @@ -36,10 +36,8 @@ public class LoginSession : Core.Network.Session { private Account account = null!; - public int LastServerTick; - public int LastClientTick; - - public int Latency; + public int ServerTick; + public int ClientTick; public LoginSession(TcpClient tcpClient, LoginServer server) : base(tcpClient) { Server = server; From 7981eff9490ca4622eccbc4aa8108638df494c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 28 Apr 2025 14:43:16 -0300 Subject: [PATCH 8/9] rabbit --- Maple2.Server.World/WorldServer.cs | 38 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index 8ff44373c..087aea010 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using Grpc.Core; using Maple2.Database.Extensions; using Maple2.Database.Storage; using Maple2.Model.Enum; @@ -57,28 +58,27 @@ public WorldServer(GameStorage gameStorage, ChannelClientLookup channelClients, private void Heartbeat() { while (!tokenSource.Token.IsCancellationRequested) { try { - Thread.Sleep(TimeSpan.FromSeconds(30)); - login.Heartbeat(new HeartbeatRequest()); - foreach (PlayerInfo playerInfo in playerInfoLookup.GetOnlinePlayerInfos()) { - if (playerInfo.CharacterId == 0) { - continue; - } - if (!channelClients.TryGetClient(playerInfo.Channel, out ChannelClient? channelClient)) { - continue; - } + Task.Delay(TimeSpan.FromSeconds(30), tokenSource.Token); - HeartbeatResponse? success = channelClient.Heartbeat(new HeartbeatRequest { - CharacterId = playerInfo.CharacterId, - }); + login.Heartbeat(new HeartbeatRequest(), cancellationToken: tokenSource.Token); - if (success != null) continue; - - playerInfo.Channel = -1; + foreach (PlayerInfo playerInfo in playerInfoLookup.GetOnlinePlayerInfos()) { + if (playerInfo.CharacterId == 0) continue; + if (!channelClients.TryGetClient(playerInfo.Channel, out ChannelClient? channel)) continue; + + try { + channel.Heartbeat(new HeartbeatRequest { + CharacterId = playerInfo.CharacterId, + }, + cancellationToken: tokenSource.Token); + } catch (RpcException) { + playerInfo.Channel = -1; + } } - } catch (ThreadInterruptedException) { - break; - } catch { - // Ignore exceptions + } catch (TaskCanceledException) { + break; // graceful shutdown + } catch (Exception ex) { + logger.Warning(ex, "Heartbeat loop error"); } } } From d9a284d4319bd96f3e61d173a2c7937d2b18e400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82ngelo=20Tadeucci?= Date: Mon, 28 Apr 2025 14:48:08 -0300 Subject: [PATCH 9/9] format --- Maple2.Server.World/WorldServer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Maple2.Server.World/WorldServer.cs b/Maple2.Server.World/WorldServer.cs index 087aea010..15ba455c4 100644 --- a/Maple2.Server.World/WorldServer.cs +++ b/Maple2.Server.World/WorldServer.cs @@ -68,8 +68,8 @@ private void Heartbeat() { try { channel.Heartbeat(new HeartbeatRequest { - CharacterId = playerInfo.CharacterId, - }, + CharacterId = playerInfo.CharacterId, + }, cancellationToken: tokenSource.Token); } catch (RpcException) { playerInfo.Channel = -1;