Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Maple2.Server.Core/proto/world/world.proto
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ service World {
rpc PlayerConfig(PlayerConfigRequest) returns (PlayerConfigResponse);
// Disconnect
rpc Disconnect(maple2.DisconnectRequest) returns (maple2.DisconnectResponse);
// Acquire and release locks for database operations.
rpc AcquireLock (LockRequest) returns (LockResponse);
rpc ReleaseLock (LockRequest) returns (LockResponse);
}

enum Server {
Expand Down Expand Up @@ -572,3 +575,11 @@ message DeathInfo {
int32 ms_remaining = 2;
int64 stop_time = 3;
}

message LockRequest {
int64 accountId = 1;
}

message LockResponse {
string error = 1;
}
4 changes: 2 additions & 2 deletions Maple2.Server.Game/Manager/Field/FieldManager/FieldManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ public bool UsePortal(GameSession session, int portalId, string password) {
session.Send(PortalPacket.MoveByPortal(session.Player, destinationCube.Position, default));
return true;
case CubePortalDestination.SelectedMap:
session.MigrateOutOfInstance(srcPortal.TargetMapId);
session.Migrate(srcPortal.TargetMapId);
return true;
case CubePortalDestination.FriendHome:
Home? home = GetHome(session, fieldPortal);
Expand All @@ -523,7 +523,7 @@ public bool UsePortal(GameSession session, int portalId, string password) {
return false;
}

session.MigrateToInstance(home.Indoor.MapId, home.Indoor.OwnerId);
session.Migrate(home.Indoor.MapId, home.Indoor.OwnerId);
return true;
}
return false;
Expand Down
2 changes: 1 addition & 1 deletion Maple2.Server.Game/PacketHandlers/GuildHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,7 @@ private void HandleEnterHouse(GameSession session) {
return;
}

session.MigrateToInstance(house.MapId, session.Guild.Id);
session.Migrate(house.MapId, session.Guild.Id);
}

private void HandleSendGift(GameSession session, IByteReader packet) {
Expand Down
2 changes: 1 addition & 1 deletion Maple2.Server.Game/PacketHandlers/HomeHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,6 @@ private void HandleWarp(GameSession session, IByteReader packet) {
session.Housing.InitNewHome(session.Player.Value.Character.Name, exportedUgcMap);
}

session.MigrateToInstance(home.Indoor.MapId, home.Indoor.OwnerId);
session.Migrate(home.Indoor.MapId, home.Indoor.OwnerId);
}
}
2 changes: 1 addition & 1 deletion Maple2.Server.Game/PacketHandlers/MoveFieldHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private void HandleVisitHome(GameSession session, IByteReader packet) {
}
}

session.MigrateToInstance(home.Indoor.MapId, home.Indoor.OwnerId);
session.Migrate(home.Indoor.MapId, home.Indoor.OwnerId);
}

private void HandleReturn(GameSession session) {
Expand Down
2 changes: 1 addition & 1 deletion Maple2.Server.Game/PacketHandlers/QuestHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ private void HandleMapleGuide(GameSession session, IByteReader packet) {
}

if (metadata.GoToMapId is Constant.DefaultHomeMapId) {
session.MigrateToInstance(Constant.DefaultHomeMapId, session.AccountId);
session.Migrate(Constant.DefaultHomeMapId, session.AccountId);
return;
}

Expand Down
121 changes: 74 additions & 47 deletions Maple2.Server.Game/Session/GameSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Runtime.CompilerServices;
using Autofac;
using Grpc.Core;
using Maple2.Database.Extensions;
Expand Down Expand Up @@ -56,6 +57,7 @@ public sealed partial class GameSession : Core.Network.Session {

#region Autofac Autowired
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedAutoPropertyAccessor.Global
public required GameStorage GameStorage { get; init; }
public required WorldClient World { get; init; }
public required ItemMetadataStorage ItemMetadata { get; init; }
Expand All @@ -73,6 +75,7 @@ public sealed partial class GameSession : Core.Network.Session {
public required ItemStatsCalculator ItemStatsCalc { private get; init; }
public required PlayerInfoStorage PlayerInfo { get; init; }
// ReSharper restore All
// ReSharper restore UnusedAutoPropertyAccessor.Global
#endregion

public ConfigManager Config { get; set; } = null!;
Expand Down Expand Up @@ -146,13 +149,19 @@ public bool EnterServer(long accountId, Guid machineId, MigrateInResponse migrat
using GameStorage.Request db = GameStorage.Context();
db.BeginTransaction();
int objectId = FieldManager.NextGlobalId();
Player? player = db.LoadPlayer(AccountId, CharacterId, objectId, GameServer.GetChannel());
Player? player;
try {
AcquireLock(CharacterId, 5);
player = db.LoadPlayer(AccountId, CharacterId, objectId, GameServer.GetChannel());
db.Commit();
} finally {
ReleaseLock(CharacterId);
}
if (player == null) {
Logger.Warning("Failed to load player from database: {AccountId}, {CharacterId}", AccountId, CharacterId);
Send(MigrationPacket.MoveResult(MigrationError.s_move_err_default));
return false;
}
db.Commit();

Player = new FieldPlayer(this, player);
Animation = new AnimationManager(this);
Expand Down Expand Up @@ -386,7 +395,7 @@ private bool PrepareFieldInternal(int mapId, out FieldManager? newField, int por
} else if (mapId == dungeonField.Lobby.MapId) {
newField = dungeonField.Lobby;
} else {
MigrateOutOfInstance(mapId);
Migrate(mapId);
newField = null;
return false;
}
Expand Down Expand Up @@ -504,7 +513,7 @@ public void ReturnField() {
character.ReturnPosition = default;

if (character.MapId is Constant.DefaultHomeMapId) {
MigrateOutOfInstance(mapId);
Migrate(mapId);
return;
}

Expand All @@ -515,7 +524,7 @@ public void ReturnField() {
houseRank.TryGetValue(Guild.Guild.HouseTheme, out GuildTable.House? house);

if (house?.MapId == character.MapId) {
MigrateOutOfInstance(mapId);
Migrate(mapId);
return;
}
}
Expand Down Expand Up @@ -659,8 +668,8 @@ public void MigrateToPlanner(PlotMode plotMode) {
}
}

public void MigrateToInstance(int mapId, long ownerId) {
bool instancedContent = ServerTableMetadata.InstanceFieldTable.Entries.ContainsKey(mapId);
public void Migrate(int mapId, long ownerId = 0) {
bool isInstanced = ServerTableMetadata.InstanceFieldTable.Entries.ContainsKey(mapId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we can remove this and just have the check happen on world server instead. just a thought. doesn't have to happen

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to save the return channel, so i think it's valid to stay in game server


try {
var request = new MigrateOutRequest {
Expand All @@ -670,13 +679,20 @@ public void MigrateToInstance(int mapId, long ownerId) {
Server = Server.World.Service.Server.Game,
MapId = mapId,
OwnerId = ownerId,
InstancedContent = instancedContent,
InstancedContent = isInstanced,
};

MigrateOutResponse response = World.MigrateOut(request);
var endpoint = new IPEndPoint(IPAddress.Parse(response.IpAddress), response.Port);
Send(MigrationPacket.GameToGame(endpoint, response.Token, mapId));
Player.Value.Character.ReturnChannel = Player.Value.Character.Channel;

if (isInstanced) {
Player.Value.Character.ReturnChannel = Player.Value.Character.Channel;
} else {
Player.Value.Character.MapId = mapId;
Player.Value.Character.ReturnChannel = 0;
}

State = SessionState.ChangeMap;
} catch (RpcException ex) {
Send(MigrationPacket.GameToGameError(MigrationError.s_move_err_default));
Expand All @@ -686,31 +702,38 @@ public void MigrateToInstance(int mapId, long ownerId) {
}
}

public void MigrateOutOfInstance(int mapId) {
try {
var request = new MigrateOutRequest {
AccountId = AccountId,
CharacterId = CharacterId,
MachineId = MachineId.ToString(),
Server = Server.World.Service.Server.Game,
Channel = Player.Value.Character.ReturnChannel,
MapId = mapId,
};
private void AcquireLock(long accountId, int maxRetries = 3) {
int retryCount = 0;
const int backoffMs = 500;

MigrateOutResponse response = World.MigrateOut(request);
var endpoint = new IPEndPoint(IPAddress.Parse(response.IpAddress), response.Port);
Send(MigrationPacket.GameToGame(endpoint, response.Token, mapId));
Player.Value.Character.MapId = mapId;
Player.Value.Character.ReturnChannel = 0;
State = SessionState.ChangeMap;
} catch (RpcException ex) {
Send(MigrationPacket.GameToGameError(MigrationError.s_move_err_default));
Send(NoticePacket.Disconnect(new InterfaceText(ex.Message)));
} finally {
Disconnect();
while (retryCount < maxRetries) {
LockResponse? response = World.AcquireLock(new LockRequest {
AccountId = accountId,
});

if (string.IsNullOrEmpty(response.Error)) {
return;
}

retryCount++;
Thread.Sleep(backoffMs);
}

Logger.Error("Failed to acquire lock for account {AccountId} after {MaxRetries} retries", accountId, maxRetries);
}

private void ReleaseLock(long accountId) {
try {
LockResponse response = World.ReleaseLock(new LockRequest {
AccountId = accountId,
});
if (!string.IsNullOrEmpty(response.Error)) {
Logger.Warning("Failed to release lock for account {AccountId}: {ErrorMessage}", accountId, response.Error);
}
} catch (RpcException ex) {
Logger.Error(ex, "Failed to release lock for account {AccountId}", accountId);
}
}

#region Dispose
~GameSession() => Dispose(false);
Expand Down Expand Up @@ -741,7 +764,28 @@ protected override void Dispose(bool disposing) {
Player.Value.Account.Online = false;
State = SessionState.Disconnected;
Complete();

SaveCacheConfig();
AcquireLock(CharacterId);
using GameStorage.Request db = GameStorage.Context();
db.BeginTransaction();
db.SavePlayer(Player);
UgcMarket.Save(db);
Config.Save(db);
Shop.Save(db);
Item.Save(db);
Survival.Save(db);
Housing.Save(db);
GameEvent.Save(db);
Achievement.Save(db);
Quest.Save(db);
Dungeon.Save(db);
db.Commit();
db.SaveChanges();
} catch (Exception ex) {
Logger.Error(ex, "Error during session cleanup for {Player}", PlayerName);
} finally {
ReleaseLock(CharacterId);
Comment thread
AngeloTadeucci marked this conversation as resolved.
Guild.Dispose();
Buddy.Dispose();
Party.Dispose();
Expand All @@ -753,23 +797,6 @@ protected override void Dispose(bool disposing) {
club.Dispose();
}

SaveCacheConfig();
using (GameStorage.Request db = GameStorage.Context()) {
db.BeginTransaction();
db.SavePlayer(Player);
UgcMarket.Save(db);
Config.Save(db);
Shop.Save(db);
Item.Save(db);
Survival.Save(db);
Housing.Save(db);
GameEvent.Save(db);
Achievement.Save(db);
Quest.Save(db);
Dungeon.Save(db);
db.Commit();
db.SaveChanges();
}
Player.Dispose();
base.Dispose(disposing);
}
Expand Down
43 changes: 42 additions & 1 deletion Maple2.Server.Login/Session/LoginSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Grpc.Core;
using Maple2.Database.Storage;
using Maple2.Model.Enum;
using Maple2.Model.Game;
Expand Down Expand Up @@ -52,6 +53,39 @@ public void Init(long accountId, Guid machineId) {
Server.OnConnected(this);
}

private void AcquireLock(long accountId, int maxRetries = 3) {
int retryCount = 0;
const int backoffMs = 500;

while (retryCount < maxRetries) {
LockResponse? response = World.AcquireLock(new LockRequest {
AccountId = accountId,
});

if (string.IsNullOrEmpty(response.Error)) {
return;
}

retryCount++;
Thread.Sleep(backoffMs);
}

Logger.Error("Failed to acquire lock for account {AccountId} after {MaxRetries} retries", accountId, maxRetries);
}

private void ReleaseLock(long accountId) {
try {
LockResponse response = World.ReleaseLock(new LockRequest {
AccountId = accountId,
});
if (!string.IsNullOrEmpty(response.Error)) {
Logger.Warning("Failed to release lock for account {AccountId}: {ErrorMessage}", accountId, response.Error);
}
} catch (RpcException ex) {
Logger.Error(ex, "Failed to release lock for account {AccountId}", accountId);
}
}

public void ListServers() {
ChannelsResponse response = World.Channels(new ChannelsRequest());
Send(BannerListPacket.Load(Server.GetSystemBanners()));
Expand All @@ -60,7 +94,14 @@ public void ListServers() {

public void ListCharacters() {
using GameStorage.Request db = GameStorage.Context();
(Account? readAccount, IList<Character>? characters) = db.ListCharacters(AccountId);
AcquireLock(AccountId);
Account? readAccount;
IList<Character>? characters;
try {
(readAccount, characters) = db.ListCharacters(AccountId);
} finally {
ReleaseLock(AccountId);
}
if (readAccount == null || characters == null) {
throw new InvalidOperationException($"Failed to load characters for account: {AccountId}");
}
Expand Down
39 changes: 39 additions & 0 deletions Maple2.Server.World/Service/WorldService.Locks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Collections.Concurrent;
using Grpc.Core;

namespace Maple2.Server.World.Service;

public partial class WorldService {
private static readonly TimeSpan LockTimeout = TimeSpan.FromSeconds(30);
private static readonly ConcurrentDictionary<long, DateTime> Locks = new ConcurrentDictionary<long, DateTime>();

public override Task<LockResponse> AcquireLock(LockRequest request, ServerCallContext context) {
DateTime now = DateTime.UtcNow;

// Remove expired lock if present
if (Locks.TryGetValue(request.AccountId, out DateTime timestamp)) {
if (now - timestamp > LockTimeout) {
Locks.TryRemove(request.AccountId, out _);
}
}

// Try to acquire lock
bool acquired = Locks.TryAdd(request.AccountId, now);
if (acquired) {
return Task.FromResult(new LockResponse());
}
return Task.FromResult(new LockResponse {
Error = "Lock already held.",
});
}

public override Task<LockResponse> ReleaseLock(LockRequest request, ServerCallContext context) {
bool removed = Locks.TryRemove(request.AccountId, out _);
if (removed) {
return Task.FromResult(new LockResponse());
}
return Task.FromResult(new LockResponse {
Error = "Lock not held.",
});
}
}