From a90b9c10b4731ad72d35f1b012c52db37c3e3a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Thu, 23 Dec 2021 16:01:45 +0100 Subject: [PATCH] feat: implement walk command handler --- .../Walk/Errors/WalkNotFinishedError.cs | 18 ++ .../CommandHandlers/Walk/WalkCancelReason.cs | 35 ++++ .../{ => Walk}/WalkCommandHandler.cs | 0 .../Walk/WalkCommandHandlerOptions.cs | 37 ++++ .../Walk/WalkPacketResponder.cs | 60 ++++++ .../CommandHandlers/Walk/WalkStatus.cs | 177 ++++++++++++++++++ 6 files changed, 327 insertions(+) create mode 100644 Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs create mode 100644 Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs rename Local/NosSmooth.LocalClient/CommandHandlers/{ => Walk}/WalkCommandHandler.cs (100%) create mode 100644 Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs create mode 100644 Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs create mode 100644 Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs diff --git a/Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs new file mode 100644 index 0000000..afcc62f --- /dev/null +++ b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs @@ -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; + +/// +/// Represents an error that can be returned from walk command handler. +/// +/// The x coordinate where the player is. (if known) +/// The y coordinate where the player is. (if known) +/// +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 diff --git a/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs new file mode 100644 index 0000000..9514d77 --- /dev/null +++ b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs @@ -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; + +/// +/// Reason of cancellation of . +/// +public enum WalkCancelReason +{ + /// + /// There was an unknown cancel reason. + /// + Unknown, + + /// + /// The user walked and CancelOnUserMove flag was set. + /// + UserWalked, + + /// + /// The map has changed during the walk was in progress. + /// + MapChanged, + + /// + /// The client was not walking for too long. + /// + NotWalkingTooLong, +} \ No newline at end of file diff --git a/Local/NosSmooth.LocalClient/CommandHandlers/WalkCommandHandler.cs b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandler.cs similarity index 100% rename from Local/NosSmooth.LocalClient/CommandHandlers/WalkCommandHandler.cs rename to Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandler.cs diff --git a/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs new file mode 100644 index 0000000..c536d8b --- /dev/null +++ b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs @@ -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 +{ + /// + /// Options for . + /// + public class WalkCommandHandlerOptions + { + /// + /// After what time to trigger not walking for too long error in milliseconds. + /// + /// + /// Use at least 2000 to avoid problems with false triggers. + /// + public int NotWalkingTooLongTrigger { get; set; } = 2000; + + /// + /// The command handler sleeps for this duration, then checks new info in milliseconds. + /// + /// + /// 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. + /// + public int CheckDelay { get; set; } = 100; + } +} diff --git a/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs new file mode 100644 index 0000000..c68552f --- /dev/null +++ b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs @@ -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; + +/// +/// Responds to to manage . +/// +public class WalkPacketResponder : IPacketResponder, IPacketResponder +{ + private readonly WalkStatus _walkStatus; + + /// + /// Initializes a new instance of the class. + /// + /// The walk status. + public WalkPacketResponder(WalkStatus walkStatus) + { + _walkStatus = walkStatus; + } + + /// + public async Task Respond(PacketEventArgs 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(); + } + + /// + public async Task Respond(PacketEventArgs 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 diff --git a/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs new file mode 100644 index 0000000..a8ea311 --- /dev/null +++ b/Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs @@ -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; + +/// +/// The status for . +/// +public class WalkStatus +{ + private readonly NostaleHookManager _hookManager; + private readonly SemaphoreSlim _semaphore; + private CancellationTokenSource? _walkingCancellation; + private bool _userCanCancel; + private bool _walkHooked; + + /// + /// Initializes a new instance of the class. + /// + /// The hooking manager. + public WalkStatus(NostaleHookManager hookManager) + { + _hookManager = hookManager; + _semaphore = new SemaphoreSlim(1, 1); + } + + /// + /// Gets if the walk command is in progress. + /// + public bool IsWalking => _walkingCancellation is not null; + + /// + /// Gets if the current walk command has been finished. + /// + public bool IsFinished { get; private set; } + + /// + /// Gets the last time of walk. + /// + public DateTimeOffset LastWalkTime { get; private set; } + + /// + /// Gets the walk target x coordinate. + /// + public int TargetX { get; private set; } + + /// + /// Gets the walk target y coordinate. + /// + public int TargetY { get; private set; } + + /// + /// Gets the characters current x coordinate. + /// + public int? CurrentX { get; private set; } + + /// + /// Gets the characters current y coordinate. + /// + public int? CurrentY { get; private set; } + + /// + /// Gets the error cause of cancellation. + /// + public WalkCancelReason? Error { get; private set; } + + /// + /// Update the time of last walk, called on WalkPacket. + /// + /// The current characters x coordinate. + /// The current characters y coordinate. + internal void UpdateWalkTime(int currentX, int currentY) + { + CurrentX = currentX; + CurrentY = currentY; + LastWalkTime = DateTimeOffset.Now; + } + + /// + /// Sets that the walk command handler is handling walk command. + /// + /// The cancellation token source for cancelling the operation. + /// The walk target x coordinate. + /// The walk target y coordinate. + /// Whether the user can cancel the operation by moving elsewhere. + /// A task that may or may not have succeeded. + 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(); + } + + /// + /// Cancel the walking token. + /// + /// The cause. + /// The cancellation token for cancelling the operation. + /// A task that may or may not have succeeded. + 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(); + } + + /// + /// Finish the walk successfully. + /// + /// The cancellation token for cancelling the operation. + /// A task that may or may not have succeeded. + 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 -- 2.48.1