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