Skip to content

Commit

Permalink
Merge pull request #516 from MUnique/dev/duel-fixes
Browse files Browse the repository at this point in the history
Fixes for duel issues
  • Loading branch information
sven-n authored Oct 29, 2024
2 parents d5cc8ce + 37fa688 commit b48093e
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 83 deletions.
129 changes: 72 additions & 57 deletions src/GameLogic/DuelRoom.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ public enum DuelState
{
Undefined,
DuelRequested,
DuelAccepted,
DuelRefused,
DuelStartFailed,
DuelAccepted,

DuelStarted,
DuelCancelled,
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -190,65 +190,29 @@ 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
{
await this.DisposeAsync().ConfigureAwait(false);
}
}

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<IDuelFinishedPlugIn>(p => p.DuelFinishedAsync(winner, loser))).ConfigureAwait(false);
}

private async ValueTask SendCurrentStateToAllPlayersAsync()
public async ValueTask CancelDuelAsync()
{
await this.AllPlayers.ForEachAsync(p => p.InvokeViewPlugInAsync<IShowDuelScoreUpdatePlugIn>(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<IDuelHealthUpdatePlugIn>(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<IDuelEndedPlugIn>(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)
Expand Down Expand Up @@ -315,7 +279,7 @@ public async ValueTask ResetAndDisposeAsync(DuelStartResult startResult)
this.State = DuelState.DuelRefused;
await this.Requester.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(startResult, this.Opponent)).ConfigureAwait(false);
}
else
else if (startResult != DuelStartResult.Undefined)
{
this.State = DuelState.DuelStartFailed;
await this.Requester.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
Expand All @@ -324,23 +288,34 @@ public async ValueTask ResetAndDisposeAsync(DuelStartResult startResult)
await this.Requester.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(startResult, this.Opponent)).ConfigureAwait(false);
await this.Opponent.InvokeViewPlugInAsync<IShowDuelRequestResultPlugIn>(p => p.ShowDuelRequestResultAsync(startResult, this.Requester)).ConfigureAwait(false);
}
else
{
await this.Requester.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
await this.Opponent.InvokeViewPlugInAsync<IDuelEndedPlugIn>(p => p.DuelEndedAsync()).ConfigureAwait(false);
}

await this.DisposeAsyncCore().ConfigureAwait(false);
}

/// <inheritdoc />
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);

await this.Requester.GameContext.DuelRoomManager.GiveBackDuelRoomAsync(this).ConfigureAwait(false);
}

this.AllPlayers.ForEach(p => p.DuelRoom = null);
this._cts.Dispose();
cts.Dispose();

await base.DisposeAsyncCore();
}
Expand All @@ -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<IDuelFinishedPlugIn>(p => p.DuelFinishedAsync(winner, loser))).ConfigureAwait(false);
}

private async ValueTask SendCurrentStateToAllPlayersAsync()
{
await this.AllPlayers.ForEachAsync(p => p.InvokeViewPlugInAsync<IShowDuelScoreUpdatePlugIn>(p => p.UpdateScoreAsync(this))).ConfigureAwait(false);

for (var index = this.Spectators.Count - 1; index >= 0; index--)
{
var spectator = this.Spectators[index];
await spectator.InvokeViewPlugInAsync<IDuelHealthUpdatePlugIn>(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<IDuelEndedPlugIn>(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);
}
}
54 changes: 34 additions & 20 deletions src/GameLogic/GameMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,29 +163,34 @@ public async ValueTask RemoveAsync(ILocateable locateable)
/// <param name="locateable">The locateable object.</param>
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)
{
Expand All @@ -205,6 +210,15 @@ public ValueTask MoveAsync(ILocateable locatable, Point target, AsyncLock moveLo
return this._areaOfInterestManager.MoveObjectAsync(locatable, target, moveLock, moveType);
}

/// <summary>
/// Initializes a respawn for the specified locateable.
/// </summary>
/// <param name="locateable">The locateable.</param>
public async ValueTask InitRespawnAsync(ILocateable locateable)
{
await this._areaOfInterestManager.RemoveObjectAsync(locateable).ConfigureAwait(false);
}

/// <summary>
/// Respawns the specified locateable.
/// </summary>
Expand Down
23 changes: 18 additions & 5 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand All @@ -521,6 +522,7 @@ public async ValueTask SetSelectedCharacterAsync(Character? character)
}

this.DuelRoom = null;
this._selectedCharacter = null;
}
else
{
Expand Down Expand Up @@ -900,7 +902,8 @@ public async ValueTask RemoveInvisibleEffectAsync()
/// <param name="gate">The gate to which the player should be moved.</param>
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;
}
Expand Down Expand Up @@ -929,7 +932,9 @@ public async ValueTask WarpToAsync(ExitGate gate)
/// <param name="gate">The gate at which the player should be respawned.</param>
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;
}
Expand Down Expand Up @@ -1661,15 +1666,23 @@ protected virtual ICustomPlugInContainer<IViewPlugIn> CreateViewPlugInContainer(
throw new NotImplementedException("CreateViewPlugInContainer must be overwritten in derived classes.");
}

private async ValueTask<bool> TryRemoveFromCurrentMapAsync()
private async ValueTask<bool> TryRemoveFromCurrentMapAsync(bool willRespawnOnSameMap)
{
var currentMap = this.CurrentMap;
if (currentMap is null)
{
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/GameLogic/Views/Duel/DuelStartResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

public enum DuelStartResult
{
Undefined,

Success,

Refused,
Expand Down
Loading

0 comments on commit b48093e

Please sign in to comment.