//
// WalkStatus.cs
//
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using NosSmooth.LocalClient.Hooks;
namespace NosSmooth.LocalClient.CommandHandlers.Walk;
/// <summary>
/// The status for <see cref="WalkCommandHandler"/>.
/// </summary>
public class WalkStatus
{
private readonly NostaleHookManager _hookManager;
private readonly SemaphoreSlim _semaphore;
private CancellationTokenSource? _walkingCancellation;
private bool _userCanCancel;
private bool _walkHooked;
/// <summary>
/// Initializes a new instance of the <see cref="WalkStatus"/> class.
/// </summary>
/// <param name="hookManager">The hooking manager.</param>
public WalkStatus(NostaleHookManager hookManager)
{
_hookManager = hookManager;
_semaphore = new SemaphoreSlim(1, 1);
}
/// <summary>
/// Gets if the walk command is in progress.
/// </summary>
public bool IsWalking => _walkingCancellation is not null;
/// <summary>
/// Gets if the current walk command has been finished.
/// </summary>
public bool IsFinished { get; private set; }
/// <summary>
/// Gets the last time of walk.
/// </summary>
public DateTimeOffset LastWalkTime { get; private set; }
/// <summary>
/// Gets the walk target x coordinate.
/// </summary>
public int TargetX { get; private set; }
/// <summary>
/// Gets the walk target y coordinate.
/// </summary>
public int TargetY { get; private set; }
/// <summary>
/// Gets the characters current x coordinate.
/// </summary>
public int? CurrentX { get; private set; }
/// <summary>
/// Gets the characters current y coordinate.
/// </summary>
public int? CurrentY { get; private set; }
/// <summary>
/// Gets the error cause of cancellation.
/// </summary>
public WalkCancelReason? Error { get; private set; }
/// <summary>
/// Update the time of last walk, called on WalkPacket.
/// </summary>
/// <param name="currentX">The current characters x coordinate.</param>
/// <param name="currentY">The current characters y coordinate.</param>
internal void UpdateWalkTime(int currentX, int currentY)
{
CurrentX = currentX;
CurrentY = currentY;
LastWalkTime = DateTimeOffset.Now;
}
/// <summary>
/// Sets that the walk command handler is handling walk command.
/// </summary>
/// <param name="cancellationTokenSource">The cancellation token source for cancelling the operation.</param>
/// <param name="targetX">The walk target x coordinate.</param>
/// <param name="targetY">The walk target y coordinate.</param>
/// <param name="userCanCancel">Whether the user can cancel the operation by moving elsewhere.</param>
/// <returns>A task that may or may not have succeeded.</returns>
internal async Task SetWalking(CancellationTokenSource cancellationTokenSource, int targetX, int targetY, bool userCanCancel)
{
await _semaphore.WaitAsync(cancellationTokenSource.Token);
if (IsWalking)
{
// Cannot call CancelWalkingAsync as that would result in a deadlock
_walkingCancellation?.Cancel();
_walkingCancellation = null;
}
IsFinished = false;
Error = null;
TargetX = targetX;
TargetY = targetY;
CurrentX = CurrentY = null;
_walkingCancellation = cancellationTokenSource;
LastWalkTime = DateTime.Now;
_userCanCancel = userCanCancel;
if (!_walkHooked)
{
_hookManager.ClientWalked += OnCharacterWalked;
_walkHooked = true;
}
_semaphore.Release();
}
/// <summary>
/// Cancel the walking token.
/// </summary>
/// <param name="error">The cause.</param>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>A task that may or may not have succeeded.</returns>
internal async Task CancelWalkingAsync(WalkCancelReason? error = null, CancellationToken ct = default)
{
await _semaphore.WaitAsync(ct);
if (!IsWalking)
{
_semaphore.Release();
return;
}
Error = error;
try
{
_walkingCancellation?.Cancel();
}
catch
{
// ignored
}
_walkingCancellation = null;
_semaphore.Release();
}
/// <summary>
/// Finish the walk successfully.
/// </summary>
/// <param name="ct">The cancellation token for cancelling the operation.</param>
/// <returns>A task that may or may not have succeeded.</returns>
internal async Task FinishWalkingAsync(CancellationToken ct = default)
{
await _semaphore.WaitAsync(ct);
IsFinished = true;
_semaphore.Release();
await CancelWalkingAsync(ct: ct);
}
private bool OnCharacterWalked(WalkEventArgs walkEventArgs)
{
if (IsWalking)
{
if (!_userCanCancel)
{
return false;
}
CancelWalkingAsync(WalkCancelReason.UserWalked)
.GetAwaiter()
.GetResult();
}
return true;
}
}