~ruther/NosSmooth

a90b9c10b4731ad72d35f1b012c52db37c3e3a94 — František Boháček 3 years ago e799024
feat: implement walk command handler
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs +18 -0
@@ 0,0 1,18 @@
//
//  WalkNotFinishedError.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 Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers.Walk.Errors;

/// <summary>
/// Represents an error that can be returned from walk command handler.
/// </summary>
/// <param name="X">The x coordinate where the player is. (if known)</param>
/// <param name="Y">The y coordinate where the player is. (if known)</param>
/// <param name="Reason"></param>
public record WalkNotFinishedError(int? X, int? Y, WalkCancelReason Reason)
    : ResultError($"Could not finish the walk to {X} {Y}, because {Reason}");
\ No newline at end of file

A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs +35 -0
@@ 0,0 1,35 @@
//
//  WalkCancelReason.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.Core.Commands;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;

/// <summary>
/// Reason of cancellation of <see cref="WalkCommand"/>.
/// </summary>
public enum WalkCancelReason
{
    /// <summary>
    /// There was an unknown cancel reason.
    /// </summary>
    Unknown,

    /// <summary>
    /// The user walked and CancelOnUserMove flag was set.
    /// </summary>
    UserWalked,

    /// <summary>
    /// The map has changed during the walk was in progress.
    /// </summary>
    MapChanged,

    /// <summary>
    /// The client was not walking for too long.
    /// </summary>
    NotWalkingTooLong,
}
\ No newline at end of file

R Local/NosSmooth.LocalClient/CommandHandlers/WalkCommandHandler.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandler.cs +0 -0
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs +37 -0
@@ 0,0 1,37 @@
//
//  WalkCommandHandlerOptions.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 System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace NosSmooth.LocalClient.CommandHandlers.Walk
{
    /// <summary>
    /// Options for <see cref="WalkCommandHandler"/>.
    /// </summary>
    public class WalkCommandHandlerOptions
    {
        /// <summary>
        /// After what time to trigger not walking for too long error in milliseconds.
        /// </summary>
        /// <remarks>
        /// Use at least 2000 to avoid problems with false triggers.
        /// </remarks>
        public int NotWalkingTooLongTrigger { get; set; } = 2000;

        /// <summary>
        /// The command handler sleeps for this duration, then checks new info in milliseconds.
        /// </summary>
        /// <remarks>
        /// The operation is done with a cancellation token, if there is an outer event, then it can be faster.
        /// Walk function is called again as well after this delay.
        /// </remarks>
        public int CheckDelay { get; set; } = 100;
    }
}

A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs +60 -0
@@ 0,0 1,60 @@
//
//  WalkPacketResponder.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 System.Security;
using NosCore.Packets.ClientPackets.Battle;
using NosCore.Packets.ClientPackets.Movement;
using NosCore.Packets.ServerPackets.MiniMap;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Packets;
using Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;

/// <summary>
/// Responds to <see cref="WalkPacket"/> to manage <see cref="WalkCommand"/>.
/// </summary>
public class WalkPacketResponder : IPacketResponder<WalkPacket>, IPacketResponder<CMapPacket>
{
    private readonly WalkStatus _walkStatus;

    /// <summary>
    /// Initializes a new instance of the <see cref="WalkPacketResponder"/> class.
    /// </summary>
    /// <param name="walkStatus">The walk status.</param>
    public WalkPacketResponder(WalkStatus walkStatus)
    {
        _walkStatus = walkStatus;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<WalkPacket> packet, CancellationToken ct = default)
    {
        if (_walkStatus.IsWalking)
        {
            _walkStatus.UpdateWalkTime(packet.Packet.XCoordinate, packet.Packet.YCoordinate);
            if (packet.Packet.XCoordinate == _walkStatus.TargetX && packet.Packet.YCoordinate == _walkStatus.TargetY)
            {
                await _walkStatus.FinishWalkingAsync(ct);
            }
        }

        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public async Task<Result> Respond(PacketEventArgs<CMapPacket> packet, CancellationToken ct = default)
    {
        if (_walkStatus.IsWalking)
        {
            await _walkStatus.CancelWalkingAsync(WalkCancelReason.MapChanged, ct);
        }

        return Result.FromSuccess();
    }

    // TODO: handle teleport on map
}
\ No newline at end of file

A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs +177 -0
@@ 0,0 1,177 @@
//
//  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;
    }
}
\ No newline at end of file

Do not follow this link