From cd55e726f1f3dc3ca63ada4f6e0148507b408c9d Mon Sep 17 00:00:00 2001 From: Acentech Dev Date: Mon, 20 Jul 2026 23:42:07 +0300 Subject: [PATCH] feat(hs): team-select handler + join action with atomic balance reserve and base warp --- .../MiniGames/HeykelSavasiContext.cs | 56 ++++++++++++++++++ .../MiniGames/HeykelSavasiJoinAction.cs | 35 ++++++++++++ .../HeykelSavasiTeamSelectHandlerPlugIn.cs | 57 +++++++++++++++++++ 3 files changed, 148 insertions(+) create mode 100644 src/GameLogic/PlayerActions/MiniGames/HeykelSavasiJoinAction.cs create mode 100644 src/GameServer/MessageHandler/MiniGames/HeykelSavasiTeamSelectHandlerPlugIn.cs diff --git a/src/GameLogic/MiniGames/HeykelSavasiContext.cs b/src/GameLogic/MiniGames/HeykelSavasiContext.cs index e00e5ce..3d88f4f 100644 --- a/src/GameLogic/MiniGames/HeykelSavasiContext.cs +++ b/src/GameLogic/MiniGames/HeykelSavasiContext.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.GameLogic.PlayerActions.MiniGames; /// /// The context of the Heykel Savasi (statue war) event. @@ -51,6 +52,12 @@ public class HeykelSavasiContext : MiniGameContext private readonly ConcurrentDictionary _teams = new(); + /// + /// Synchronizes the check-then-act sequence of , so that concurrent + /// join attempts cannot both pass the balance check before either of them reserves a slot. + /// + private readonly object _teamLock = new(); + /// /// Initializes a new instance of the class. /// @@ -125,6 +132,55 @@ public class HeykelSavasiContext : MiniGameContext /// All players assigned to . public IEnumerable PlayersOf(HeykelSavasiTeam team) => this._teams.Where(kv => kv.Value == team).Select(kv => kv.Key); + /// + /// Atomically attempts to join the given player to the given team, reserving a slot under + /// to fix a check-then-act race between the balance check and the team + /// assignment, then entering the mini game. If entering fails, the reservation is rolled back. + /// + /// The player who wants to join. + /// The team which the player wants to join. + /// true if the player successfully joined and entered the mini game; otherwise, false. + public async ValueTask TryJoinTeamAsync(Player player, HeykelSavasiTeam team) + { + bool reserved; + lock (this._teamLock) + { + reserved = this.State == MiniGameState.Open + && this.GetTeam(player) == HeykelSavasiTeam.None + && this.CanJoin(team); + if (reserved) + { + // Reserve the slot synchronously so a concurrent join sees the updated team count. + this.AssignTeam(player, team); + } + } + + if (!reserved) + { + return false; + } + + var enterResult = await this.TryEnterAsync(player).ConfigureAwait(false); + if (enterResult != EnterResult.Success) + { + lock (this._teamLock) + { + // Roll back the reservation; the player never actually entered the mini game. + this.RemoveTeam(player); + } + + return false; + } + + return true; + } + + /// + /// Removes the given player's team reservation, if any. + /// + /// The player. + private void RemoveTeam(Player player) => this._teams.TryRemove(player, out _); + /// /// /// Cancels the event if either team is empty at start (no opponent to fight); otherwise diff --git a/src/GameLogic/PlayerActions/MiniGames/HeykelSavasiJoinAction.cs b/src/GameLogic/PlayerActions/MiniGames/HeykelSavasiJoinAction.cs new file mode 100644 index 0000000..62c2f0e --- /dev/null +++ b/src/GameLogic/PlayerActions/MiniGames/HeykelSavasiJoinAction.cs @@ -0,0 +1,35 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlayerActions.MiniGames; + +using MUnique.OpenMU.GameLogic.MiniGames; +using MUnique.OpenMU.GameLogic.Views; +using MUnique.OpenMU.Interfaces; + +/// +/// Player action which implements joining a team of the Heykel Savasi (statue war) event. +/// +public class HeykelSavasiJoinAction +{ + /// + /// Tries to join the given player to the given team of the Heykel Savasi event, and warps the + /// player to the team's base spawn gate on success. + /// + /// The player who wants to join. + /// The team which the player wants to join. + /// The currently open Heykel Savasi context. + public async ValueTask JoinAsync(Player player, HeykelSavasiTeam team, HeykelSavasiContext context) + { + if (await context.TryJoinTeamAsync(player, team).ConfigureAwait(false)) + { + await player.WarpToAsync(context.GetTeamSpawnGate(team)).ConfigureAwait(false); + } + else + { + await player.InvokeViewPlugInAsync(p => + p.ShowMessageAsync("Takima simdi katilamadin (denge/durum).", MessageType.BlueNormal)).ConfigureAwait(false); + } + } +} diff --git a/src/GameServer/MessageHandler/MiniGames/HeykelSavasiTeamSelectHandlerPlugIn.cs b/src/GameServer/MessageHandler/MiniGames/HeykelSavasiTeamSelectHandlerPlugIn.cs new file mode 100644 index 0000000..da76d98 --- /dev/null +++ b/src/GameServer/MessageHandler/MiniGames/HeykelSavasiTeamSelectHandlerPlugIn.cs @@ -0,0 +1,57 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameServer.MessageHandler.MiniGames; + +using System; +using System.Runtime.InteropServices; +using MUnique.OpenMU.GameLogic; +using MUnique.OpenMU.GameLogic.MiniGames; +using MUnique.OpenMU.GameLogic.PlayerActions.MiniGames; +using MUnique.OpenMU.Network.Packets.ClientToServer; +using MUnique.OpenMU.PlugIns; + +/// +/// Handler for the Heykel Savasi (statue war) team-select request packets. +/// +[PlugIn] +[Guid("49D4C881-B0CD-44A5-9341-FDF1B432B602")] +public class HeykelSavasiTeamSelectHandlerPlugIn : IPacketHandlerPlugIn +{ + /// + /// The game action which contains the logic to join a team of the event. + /// + private readonly HeykelSavasiJoinAction _joinAction = new(); + + /// + public bool IsEncryptionExpected => true; + + /// + public byte Key => HeykelSavasiTeamSelect.Code; + + /// + public async ValueTask HandlePacketAsync(Player player, Memory packet) + { + HeykelSavasiTeamSelect request = packet; + var team = request.Team switch + { + 1 => HeykelSavasiTeam.Red, + 2 => HeykelSavasiTeam.Blue, + _ => HeykelSavasiTeam.None, + }; + + if (team == HeykelSavasiTeam.None) + { + return; + } + + var context = player.GameContext is GameContext gameContext + ? await gameContext.GetOpenHeykelSavasiAsync().ConfigureAwait(false) + : null; + if (context is not null) + { + await this._joinAction.JoinAsync(player, team, context).ConfigureAwait(false); + } + } +}