From b36a2df3be8c3262f49a67a2f5223a07015f966a Mon Sep 17 00:00:00 2001 From: sven-n Date: Mon, 28 Oct 2024 20:14:49 +0100 Subject: [PATCH 1/3] Fixed duel issues: * After first round, players couldn't attack each other anymore. The reason was changing player ids after respawn. Now the player ids are constant when a player respawns on the same game map. * Refactored the end of a duel, so that the players are moved to the safezone after anything ends the duel. --- src/GameLogic/DuelRoom.cs | 129 ++++++++++-------- src/GameLogic/GameMap.cs | 54 +++++--- src/GameLogic/Player.cs | 23 +++- .../Duel/EndDuelWhenLeavingDuelMapPlugIn.cs | 3 +- src/GameLogic/Views/Duel/DuelStartResult.cs | 2 + 5 files changed, 128 insertions(+), 83 deletions(-) diff --git a/src/GameLogic/DuelRoom.cs b/src/GameLogic/DuelRoom.cs index b46c46dff..fe40a94b1 100644 --- a/src/GameLogic/DuelRoom.cs +++ b/src/GameLogic/DuelRoom.cs @@ -10,9 +10,9 @@ public enum DuelState { Undefined, DuelRequested, - DuelAccepted, DuelRefused, DuelStartFailed, + DuelAccepted, DuelStarted, DuelCancelled, @@ -22,7 +22,7 @@ public enum DuelState public sealed class DuelRoom : AsyncDisposable { private readonly AsyncLock _spectatorLock = new(); - private readonly CancellationTokenSource _cts = new(); + private CancellationTokenSource? _cts = new(); private byte _scoreRequester; private byte _scoreOpponent; private int _maximumScore; @@ -143,7 +143,7 @@ public async Task RunDuelAsync() { try { - var cancellationToken = this._cts.Token; + var cancellationToken = this._cts?.Token ?? default; // We first wait until both players are on the map while (this.Requester.Id == default || this.Opponent.Id == default) @@ -190,14 +190,13 @@ public async Task RunDuelAsync() await this.FinishDuelAsync().ConfigureAwait(false); } } - catch (OperationCanceledException) - { - // We expect that, when it's cancelled from outside. - // So we just do nothing in this case. - } - catch (Exception ex) + catch { - await this.CancelDuelAsync().ConfigureAwait(false); + if (this.State is not DuelState.DuelFinished) + { + this.State = DuelState.DuelCancelled; + await this.StopDuelAsync().ConfigureAwait(false); + } } finally { @@ -205,50 +204,15 @@ public async Task RunDuelAsync() } } - private async ValueTask NotifyDuelFinishedAsync() - { - var winner = this.ScoreRequester > this.ScoreOpponent ? this.Requester : this.Opponent; - var loser = this.Requester == winner ? this.Opponent : this.Requester; - await this.AllPlayers.ForEachAsync(player => player.InvokeViewPlugInAsync(p => p.DuelFinishedAsync(winner, loser))).ConfigureAwait(false); - } - - private async ValueTask SendCurrentStateToAllPlayersAsync() + public async ValueTask CancelDuelAsync() { - await this.AllPlayers.ForEachAsync(p => p.InvokeViewPlugInAsync(p => p.UpdateScoreAsync(this))).ConfigureAwait(false); - - for (var index = this.Spectators.Count - 1; index >= 0; index--) + if (this._cts is { } cts) { - var spectator = this.Spectators[index]; - await spectator.InvokeViewPlugInAsync(p => p.UpdateHealthAsync(this)).ConfigureAwait(false); + await cts.CancelAsync().ConfigureAwait(false); + } } - private async ValueTask FinishDuelAsync() - { - await this.Opponent.ResetPetBehaviorAsync().ConfigureAwait(false); - await this.Requester.ResetPetBehaviorAsync().ConfigureAwait(false); - - await this.NotifyDuelFinishedAsync().ConfigureAwait(false); - await Task.Delay(10000, default).ConfigureAwait(false); - await this.MovePlayersToExit().ConfigureAwait(false); - } - - public async ValueTask StopDuelAsync() - { - await this.Opponent.ResetPetBehaviorAsync().ConfigureAwait(false); - await this.Requester.ResetPetBehaviorAsync().ConfigureAwait(false); - - await this.ResetAndDisposeAsync(DuelStartResult.Refused).ConfigureAwait(false); - await this.AllPlayers.ForEachAsync(player => player.InvokeViewPlugInAsync(p => p.DuelEndedAsync())).ConfigureAwait(false); - } - - public async ValueTask CancelDuelAsync() - { - await this._cts.CancelAsync().ConfigureAwait(false); - - await this.StopDuelAsync().ConfigureAwait(false); - } - public ExitGate? GetSpawnGate(Player player) { if (this.Opponent == player) @@ -315,7 +279,7 @@ public async ValueTask ResetAndDisposeAsync(DuelStartResult startResult) this.State = DuelState.DuelRefused; await this.Requester.InvokeViewPlugInAsync(p => p.ShowDuelRequestResultAsync(startResult, this.Opponent)).ConfigureAwait(false); } - else + else if (startResult != DuelStartResult.Undefined) { this.State = DuelState.DuelStartFailed; await this.Requester.InvokeViewPlugInAsync(p => p.DuelEndedAsync()).ConfigureAwait(false); @@ -324,6 +288,11 @@ public async ValueTask ResetAndDisposeAsync(DuelStartResult startResult) await this.Requester.InvokeViewPlugInAsync(p => p.ShowDuelRequestResultAsync(startResult, this.Opponent)).ConfigureAwait(false); await this.Opponent.InvokeViewPlugInAsync(p => p.ShowDuelRequestResultAsync(startResult, this.Requester)).ConfigureAwait(false); } + else + { + await this.Requester.InvokeViewPlugInAsync(p => p.DuelEndedAsync()).ConfigureAwait(false); + await this.Opponent.InvokeViewPlugInAsync(p => p.DuelEndedAsync()).ConfigureAwait(false); + } await this.DisposeAsyncCore().ConfigureAwait(false); } @@ -331,8 +300,14 @@ public async ValueTask ResetAndDisposeAsync(DuelStartResult startResult) /// protected override async ValueTask DisposeAsyncCore() { - await this._cts.CancelAsync().ConfigureAwait(false); - if (this.State >= DuelState.DuelStarted) + if (Interlocked.Exchange(ref this._cts, null) is not { } cts) + { + return; + } + + await cts.CancelAsync().ConfigureAwait(false); + + if (this.State >= DuelState.DuelAccepted) { await this.MovePlayersToExit().ConfigureAwait(false); @@ -340,7 +315,7 @@ protected override async ValueTask DisposeAsyncCore() } this.AllPlayers.ForEach(p => p.DuelRoom = null); - this._cts.Dispose(); + cts.Dispose(); await base.DisposeAsyncCore(); } @@ -356,18 +331,58 @@ private async ValueTask MovePlayersToExit() foreach (var player in players) { - if (exitGate is not null && !this.IsDuelist(player)) + try { - await player.WarpToAsync(exitGate).ConfigureAwait(false); + if (exitGate is not null && !this.IsDuelist(player)) + { + await player.WarpToAsync(exitGate).ConfigureAwait(false); + } + else + { + await player.WarpToSafezoneAsync().ConfigureAwait(false); + } } - else + catch (Exception ex) { - await player.WarpToSafezoneAsync().ConfigureAwait(false); + player.Logger.LogError(ex, "Unexpected error when moving player away from duel arena."); } } } + private async ValueTask NotifyDuelFinishedAsync() + { + var winner = this.ScoreRequester > this.ScoreOpponent ? this.Requester : this.Opponent; + var loser = this.Requester == winner ? this.Opponent : this.Requester; + await this.AllPlayers.ForEachAsync(player => player.InvokeViewPlugInAsync(p => p.DuelFinishedAsync(winner, loser))).ConfigureAwait(false); + } + private async ValueTask SendCurrentStateToAllPlayersAsync() + { + await this.AllPlayers.ForEachAsync(p => p.InvokeViewPlugInAsync(p => p.UpdateScoreAsync(this))).ConfigureAwait(false); + for (var index = this.Spectators.Count - 1; index >= 0; index--) + { + var spectator = this.Spectators[index]; + await spectator.InvokeViewPlugInAsync(p => p.UpdateHealthAsync(this)).ConfigureAwait(false); + } + } + + private async ValueTask StopDuelAsync() + { + await this.Opponent.ResetPetBehaviorAsync().ConfigureAwait(false); + await this.Requester.ResetPetBehaviorAsync().ConfigureAwait(false); + + await this.ResetAndDisposeAsync(DuelStartResult.Undefined).ConfigureAwait(false); + await this.AllPlayers.ForEachAsync(player => player.InvokeViewPlugInAsync(p => p.DuelEndedAsync())).ConfigureAwait(false); + } + + private async ValueTask FinishDuelAsync() + { + await this.Opponent.ResetPetBehaviorAsync().ConfigureAwait(false); + await this.Requester.ResetPetBehaviorAsync().ConfigureAwait(false); + await this.NotifyDuelFinishedAsync().ConfigureAwait(false); + await Task.Delay(10000, default).ConfigureAwait(false); + await this.MovePlayersToExit().ConfigureAwait(false); + } } \ No newline at end of file diff --git a/src/GameLogic/GameMap.cs b/src/GameLogic/GameMap.cs index b346b6b28..a1a00b2cc 100644 --- a/src/GameLogic/GameMap.cs +++ b/src/GameLogic/GameMap.cs @@ -163,29 +163,34 @@ public async ValueTask RemoveAsync(ILocateable locateable) /// The locateable object. public async ValueTask AddAsync(ILocateable locateable) { - switch (locateable) + if (!this._objectsInMap.TryGetValue(locateable.Id, out var existing) + || existing != locateable) { - case DroppedItem droppedItem: - droppedItem.Id = (ushort)this._dropIdGenerator.GenerateId(); - break; - case DroppedMoney droppedMoney: - droppedMoney.Id = (ushort)this._dropIdGenerator.GenerateId(); - break; - case Player player: - player.Id = (ushort)this._objectIdGenerator.GenerateId(); - Interlocked.Increment(ref this._playerCount); - break; - case NonPlayerCharacter npc: - npc.Id = (ushort)this._objectIdGenerator.GenerateId(); - break; - case ISupportIdUpdate idUpdate: - idUpdate.Id = (ushort)this._objectIdGenerator.GenerateId(); - break; - default: - throw new ArgumentException($"Adding an object of type {locateable.GetType()} is not supported."); + switch (locateable) + { + case DroppedItem droppedItem: + droppedItem.Id = (ushort)this._dropIdGenerator.GenerateId(); + break; + case DroppedMoney droppedMoney: + droppedMoney.Id = (ushort)this._dropIdGenerator.GenerateId(); + break; + case Player player: + player.Id = (ushort)this._objectIdGenerator.GenerateId(); + Interlocked.Increment(ref this._playerCount); + break; + case NonPlayerCharacter npc: + npc.Id = (ushort)this._objectIdGenerator.GenerateId(); + break; + case ISupportIdUpdate idUpdate: + idUpdate.Id = (ushort)this._objectIdGenerator.GenerateId(); + break; + default: + throw new ArgumentException($"Adding an object of type {locateable.GetType()} is not supported."); + } + + this._objectsInMap.Add(locateable.Id, locateable); } - this._objectsInMap.Add(locateable.Id, locateable); await this._areaOfInterestManager.AddObjectAsync(locateable).ConfigureAwait(false); if (this.ObjectAdded is { } eventHandler) { @@ -205,6 +210,15 @@ public ValueTask MoveAsync(ILocateable locatable, Point target, AsyncLock moveLo return this._areaOfInterestManager.MoveObjectAsync(locatable, target, moveLock, moveType); } + /// + /// Initializes a respawn for the specified locateable. + /// + /// The locateable. + public async ValueTask InitRespawnAsync(ILocateable locateable) + { + await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false); + } + /// /// Respawns the specified locateable. /// diff --git a/src/GameLogic/Player.cs b/src/GameLogic/Player.cs index 6d0ae48e4..48402078f 100644 --- a/src/GameLogic/Player.cs +++ b/src/GameLogic/Player.cs @@ -18,6 +18,7 @@ namespace MUnique.OpenMU.GameLogic; using MUnique.OpenMU.GameLogic.PlugIns; using MUnique.OpenMU.GameLogic.Views; using MUnique.OpenMU.GameLogic.Views.Character; +using MUnique.OpenMU.GameLogic.Views.Duel; using MUnique.OpenMU.GameLogic.Views.Inventory; using MUnique.OpenMU.GameLogic.Views.MuHelper; using MUnique.OpenMU.GameLogic.Views.Pet; @@ -507,7 +508,7 @@ public async ValueTask SetSelectedCharacterAsync(Character? character) this._appearanceData.RaiseAppearanceChanged(); await this.PlayerLeftWorld.SafeInvokeAsync(this).ConfigureAwait(false); - this._selectedCharacter = null; + (this.SkillList as IDisposable)?.Dispose(); this.SkillList = null; @@ -521,6 +522,7 @@ public async ValueTask SetSelectedCharacterAsync(Character? character) } this.DuelRoom = null; + this._selectedCharacter = null; } else { @@ -900,7 +902,8 @@ public async ValueTask RemoveInvisibleEffectAsync() /// The gate to which the player should be moved. public async ValueTask WarpToAsync(ExitGate gate) { - if (!await this.TryRemoveFromCurrentMapAsync().ConfigureAwait(false)) + var isRespawnOnSameMap = object.Equals(this.CurrentMap?.Definition, gate.Map); + if (!await this.TryRemoveFromCurrentMapAsync(isRespawnOnSameMap).ConfigureAwait(false)) { return; } @@ -929,7 +932,9 @@ public async ValueTask WarpToAsync(ExitGate gate) /// The gate at which the player should be respawned. public async ValueTask RespawnAtAsync(ExitGate gate) { - if (!await this.TryRemoveFromCurrentMapAsync().ConfigureAwait(false)) + var isRespawnOnSameMap = object.Equals(this.CurrentMap?.Definition, gate.Map); + + if (!await this.TryRemoveFromCurrentMapAsync(isRespawnOnSameMap).ConfigureAwait(false)) { return; } @@ -1661,7 +1666,7 @@ protected virtual ICustomPlugInContainer CreateViewPlugInContainer( throw new NotImplementedException("CreateViewPlugInContainer must be overwritten in derived classes."); } - private async ValueTask TryRemoveFromCurrentMapAsync() + private async ValueTask TryRemoveFromCurrentMapAsync(bool willRespawnOnSameMap) { var currentMap = this.CurrentMap; if (currentMap is null) @@ -1669,7 +1674,15 @@ private async ValueTask TryRemoveFromCurrentMapAsync() return false; } - await currentMap.RemoveAsync(this).ConfigureAwait(false); + if (willRespawnOnSameMap) + { + await currentMap.InitRespawnAsync(this).ConfigureAwait(false); + } + else + { + await currentMap.RemoveAsync(this).ConfigureAwait(false); + } + this.IsAlive = false; this.IsTeleporting = false; await this._walker.StopAsync().ConfigureAwait(false); diff --git a/src/GameLogic/PlayerActions/Duel/EndDuelWhenLeavingDuelMapPlugIn.cs b/src/GameLogic/PlayerActions/Duel/EndDuelWhenLeavingDuelMapPlugIn.cs index e69f663d4..401dcc535 100644 --- a/src/GameLogic/PlayerActions/Duel/EndDuelWhenLeavingDuelMapPlugIn.cs +++ b/src/GameLogic/PlayerActions/Duel/EndDuelWhenLeavingDuelMapPlugIn.cs @@ -33,10 +33,11 @@ public async ValueTask ObjectRemovedFromMapAsync(GameMap map, ILocateable remove var removedFromDuelMap = duelRoom.Area.FirstPlayerGate?.Map == map.Definition; if (removedFromDuelMap && duelRoom.IsDuelist(player) - && duelRoom.State is DuelState.DuelStarted + && duelRoom.State is (DuelState.DuelStarted or DuelState.DuelAccepted) && player.IsAlive) { await duelRoom.CancelDuelAsync().ConfigureAwait(false); + return; } diff --git a/src/GameLogic/Views/Duel/DuelStartResult.cs b/src/GameLogic/Views/Duel/DuelStartResult.cs index 84ae1811b..3db6edc66 100644 --- a/src/GameLogic/Views/Duel/DuelStartResult.cs +++ b/src/GameLogic/Views/Duel/DuelStartResult.cs @@ -2,6 +2,8 @@ public enum DuelStartResult { + Undefined, + Success, Refused, From 6da48a25d461bdfd296fe6aa562ded9a6258e678 Mon Sep 17 00:00:00 2001 From: sven-n Date: Mon, 28 Oct 2024 20:15:11 +0100 Subject: [PATCH 2/3] Set Lorencia as Safezone for the Duel Arena --- .../Initialization/VersionSeasonSix/Maps/DuelArena.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/DuelArena.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/DuelArena.cs index ea8be42c5..72b71c6a9 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/Maps/DuelArena.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/DuelArena.cs @@ -36,4 +36,7 @@ public DuelArena(IContext context, GameConfiguration gameConfiguration) /// protected override string MapName => Name; + + /// + protected override byte SafezoneMapNumber => Lorencia.Number; } \ No newline at end of file From 37fa688fa94a0e3add6ee6899edfd6e7a7d8df02 Mon Sep 17 00:00:00 2001 From: sven-n Date: Mon, 28 Oct 2024 20:20:30 +0100 Subject: [PATCH 3/3] Fix Duel Arena safezone map --- .../Updates/FixDuelArenaSafezoneMapUpdate.cs | 53 +++++++++++++++++++ .../Initialization/Updates/UpdateVersion.cs | 5 ++ 2 files changed, 58 insertions(+) create mode 100644 src/Persistence/Initialization/Updates/FixDuelArenaSafezoneMapUpdate.cs diff --git a/src/Persistence/Initialization/Updates/FixDuelArenaSafezoneMapUpdate.cs b/src/Persistence/Initialization/Updates/FixDuelArenaSafezoneMapUpdate.cs new file mode 100644 index 000000000..d1f6f3065 --- /dev/null +++ b/src/Persistence/Initialization/Updates/FixDuelArenaSafezoneMapUpdate.cs @@ -0,0 +1,53 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.Persistence.Initialization.Updates; + +using System.Runtime.InteropServices; +using MUnique.OpenMU.DataModel.Configuration; +using MUnique.OpenMU.Persistence.Initialization.VersionSeasonSix.Maps; +using MUnique.OpenMU.PlugIns; + +/// +/// This Sets the safezone of duel arena to lorencia. +/// +[PlugIn(PlugInName, PlugInDescription)] +[Guid("27714BB3-43F9-4D90-920F-98EF0EC20232")] +public class FixDuelArenaSafezoneMapUpdate : UpdatePlugInBase +{ + /// + /// The plug in name. + /// + internal const string PlugInName = "Fix Duel Arena Safezone Map"; + + /// + /// The plug in description. + /// + internal const string PlugInDescription = "Sets the safezone of duel arena to lorencia."; + + /// + public override UpdateVersion Version => UpdateVersion.FixDuelArenaSafezoneMap; + + /// + public override string DataInitializationKey => VersionSeasonSix.DataInitialization.Id; + + /// + public override string Name => PlugInName; + + /// + public override string Description => PlugInDescription; + + /// + public override bool IsMandatory => false; + + /// + public override DateTime CreatedAt => new(2024, 10, 28, 18, 0, 0, DateTimeKind.Utc); + + /// + protected override async ValueTask ApplyAsync(IContext context, GameConfiguration gameConfiguration) + { + var duelArena = gameConfiguration.Maps.First(x => x.Number == DuelArena.Number); + duelArena.SafezoneMap = gameConfiguration.Maps.First(m => m.Number == Lorencia.Number); + } +} \ No newline at end of file diff --git a/src/Persistence/Initialization/Updates/UpdateVersion.cs b/src/Persistence/Initialization/Updates/UpdateVersion.cs index b8b01eee3..d6e46e038 100644 --- a/src/Persistence/Initialization/Updates/UpdateVersion.cs +++ b/src/Persistence/Initialization/Updates/UpdateVersion.cs @@ -184,4 +184,9 @@ public enum UpdateVersion /// The version of the . /// AddHarmonyOptionWeightsSeason6 = 35, + + /// + /// The version of the . + /// + FixDuelArenaSafezoneMap = 36, } \ No newline at end of file