~ruther/NosSmooth

60baf8dfdae217778e2353fd302048d38b150aa6 — Rutherther 3 years ago 0723002
feat(localclient): use take control command in local client
R Core/NosSmooth.Core/Commands/WalkCommand.cs => Core/NosSmooth.Core/Commands/Walking/PlayerWalkCommand.cs +25 -16
@@ 1,16 1,25 @@
//
//  WalkCommand.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.

namespace NosSmooth.Core.Commands;

/// <summary>
/// Command that moves the player to the specified target position.
/// May be used only in world.
/// </summary>
/// <param name="TargetX">The x coordinate of the target position to move to.</param>
/// <param name="TargetY">The y coordinate of the target position to move to.</param>
/// <param name="CancelOnUserMove">Whether to cancel the walk when the user clicks to move somewhere.</param>
public record WalkCommand(ushort TargetX, ushort TargetY, bool CancelOnUserMove = true) : ICommand;
\ No newline at end of file
//
//  PlayerWalkCommand.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.Control;

namespace NosSmooth.Core.Commands.Walking;

/// <summary>
/// Command that moves the player to the specified target position.
/// May be used only in world.
/// </summary>
/// <param name="TargetX">The x coordinate of the target position to move to.</param>
/// <param name="TargetY">The y coordinate of the target position to move to.</param>
public record PlayerWalkCommand
(
    ushort TargetX,
    ushort TargetY,
    bool CanBeCancelledByAnother = true,
    bool WaitForCancellation = true,
    bool AllowUserCancel = true,
    bool CancelOnMapChange = true
) : ICommand, ITakeControlCommand;
\ No newline at end of file

M Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs +1 -1
@@ 14,5 14,5 @@ namespace NosSmooth.LocalClient.CommandHandlers.Walk.Errors;
/// <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)
public record WalkNotFinishedError(int? X, int? Y, WalkUnfinishedReason Reason)
    : ResultError($"Could not finish the walk to {X} {Y}, because {Reason}");
\ No newline at end of file

R Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandler.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/PlayerWalkCommandHandler.cs +143 -96
@@ 1,97 1,144 @@
//
//  WalkCommandHandler.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 Microsoft.Extensions.Options;
using NosSmooth.Core.Commands;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalClient.CommandHandlers.Walk.Errors;
using Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;

/// <summary>
/// Handles <see cref="WalkCommand"/>.
/// </summary>
public class WalkCommandHandler : ICommandHandler<WalkCommand>
{
    private readonly PlayerManagerBinding _playerManagerBinding;
    private readonly WalkStatus _walkStatus;
    private readonly WalkCommandHandlerOptions _options;

    /// <summary>
    /// Initializes a new instance of the <see cref="WalkCommandHandler"/> class.
    /// </summary>
    /// <param name="playerManagerBinding">The character object binding.</param>
    /// <param name="walkStatus">The walk status.</param>
    /// <param name="options">The options.</param>
    public WalkCommandHandler(PlayerManagerBinding playerManagerBinding, WalkStatus walkStatus, IOptions<WalkCommandHandlerOptions> options)
    {
        _options = options.Value;
        _playerManagerBinding = playerManagerBinding;
        _walkStatus = walkStatus;
    }

    /// <inheritdoc/>
    /// 1) If client called walk, cancel.
    /// 2) If another walk command requested, cancel.
    /// 3) If at the correct spot, cancel.
    /// 4) If not walking for over x ms, cancel.
    public async Task<Result> HandleCommand(WalkCommand command, CancellationToken ct = default)
    {
        CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct);
        ct = linked.Token;
        await _walkStatus.SetWalking(linked, command.TargetX, command.TargetY, command.CancelOnUserMove);
        while (!ct.IsCancellationRequested)
        {
            var walkResult = _playerManagerBinding.Walk(command.TargetX, command.TargetY);
            if (!walkResult.IsSuccess)
            {
                try
                {
                    await _walkStatus.CancelWalkingAsync(ct: ct);
                }
                catch
                {
                    // ignored, just for cancellation
                }

                return Result.FromError(walkResult);
            }

            if (walkResult.Entity == false)
            {
                await _walkStatus.CancelWalkingAsync(WalkCancelReason.NosTaleReturnedFalse);
                return new WalkNotFinishedError(_walkStatus.CurrentX, _walkStatus.CurrentY, WalkCancelReason.NosTaleReturnedFalse);
            }
            try
            {
                await Task.Delay(_options.CheckDelay, ct);
            }
            catch
            {
                // ignored
            }

            if (_walkStatus.IsFinished)
            {
                return Result.FromSuccess();
            }

            if (_walkStatus.Error is not null)
            {
                return new WalkNotFinishedError(_walkStatus.CurrentX, _walkStatus.CurrentY, (WalkCancelReason)_walkStatus.Error);
            }

            if ((DateTimeOffset.Now - _walkStatus.LastWalkTime).TotalMilliseconds > _options.NotWalkingTooLongTrigger)
            {
                await _walkStatus.CancelWalkingAsync(WalkCancelReason.NotWalkingTooLong);
                return new WalkNotFinishedError(_walkStatus.CurrentX, _walkStatus.CurrentY, WalkCancelReason.NotWalkingTooLong);
            }
        }

        return new WalkNotFinishedError(_walkStatus.CurrentX, _walkStatus.CurrentY, WalkCancelReason.Unknown);
    }
//
//  PlayerWalkCommandHandler.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 Microsoft.Extensions.Options;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Commands.Control;
using NosSmooth.Core.Commands.Walking;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalClient.CommandHandlers.Walk.Errors;
using Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;

/// <summary>
/// Handles <see cref="PlayerWalkCommand"/>.
/// </summary>
public class PlayerWalkCommandHandler : ICommandHandler<PlayerWalkCommand>
{
    /// <summary>
    /// Group that is used for <see cref="TakeControlCommand"/>.
    /// </summary>
    public const string PlayerWalkControlGroup = "PlayerWalk";

    private readonly PlayerManagerBinding _playerManagerBinding;
    private readonly INostaleClient _nostaleClient;
    private readonly WalkCommandHandlerOptions _options;

    private ushort _x;
    private ushort _y;

    /// <summary>
    /// Initializes a new instance of the <see cref="PlayerWalkCommandHandler"/> class.
    /// </summary>
    /// <param name="playerManagerBinding">The character object binding.</param>
    /// <param name="nostaleClient">The nostale client.</param>
    /// <param name="options">The options.</param>
    public PlayerWalkCommandHandler
    (
        PlayerManagerBinding playerManagerBinding,
        INostaleClient nostaleClient,
        IOptions<WalkCommandHandlerOptions> options
    )
    {
        _options = options.Value;
        _playerManagerBinding = playerManagerBinding;
        _nostaleClient = nostaleClient;
    }

    /// <inheritdoc/>
    /// 1) If client called walk, cancel.
    /// 2) If another walk command requested, cancel.
    /// 3) If at the correct spot, cancel.
    /// 4) If not walking for over x ms, cancel.
    public async Task<Result> HandleCommand(PlayerWalkCommand command, CancellationToken ct = default)
    {
        _x = command.TargetX;
        _y = command.TargetY;

        using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(ct);
        WalkUnfinishedReason? reason = null;
        var takeControlCommand = command.CreateTakeControl
        (
            PlayerWalkControlGroup,
            WalkGrantedCallback,
            (r) =>
            {
                reason = r switch
                {
                    ControlCancelReason.AnotherTask => WalkUnfinishedReason.AnotherTask,
                    ControlCancelReason.UserAction => WalkUnfinishedReason.UserAction,
                    _ => WalkUnfinishedReason.Unknown
                };
                return Task.FromResult(Result.FromSuccess());
            }
        );

        var commandResult = await _nostaleClient.SendCommandAsync(takeControlCommand, ct);
        if (!commandResult.IsSuccess)
        {
            return commandResult;
        }

        if (reason is null && !IsAt(command.TargetX, command.TargetY))
        {
            reason = WalkUnfinishedReason.PathNotFound;
        }

        if (reason is null)
        {
            return Result.FromSuccess();
        }

        return new WalkNotFinishedError
        (
            _playerManagerBinding.PlayerManager.X,
            _playerManagerBinding.PlayerManager.Y,
            (WalkUnfinishedReason)reason
        );
    }

    private bool IsAtTarget()
    {
        return _playerManagerBinding.PlayerManager.TargetX == _playerManagerBinding.PlayerManager.Player.X
            && _playerManagerBinding.PlayerManager.TargetY == _playerManagerBinding.PlayerManager.Player.Y;
    }

    private bool IsAt(ushort x, ushort y)
    {
        return _playerManagerBinding.PlayerManager.Player.X == x && _playerManagerBinding.PlayerManager.Player.Y == y;
    }

    private async Task<Result> WalkGrantedCallback(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var result = _playerManagerBinding.Walk(_x, _y);
            if (!result.IsSuccess)
            {
                return Result.FromError(result);
            }

            try
            {
                await Task.Delay(_options.CheckDelay, ct);
            }
            catch
            {
                // ignored
            }

            if (IsAtTarget() || IsAt(_x, _y))
            {
                return Result.FromSuccess();
            }
        }

        return Result.FromSuccess(); // cancellation is handled in cancellation callback.
    }
}
\ No newline at end of file

M Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs +3 -11
@@ 7,25 7,17 @@
namespace NosSmooth.LocalClient.CommandHandlers.Walk
{
    /// <summary>
    /// Options for <see cref="WalkCommandHandler"/>.
    /// Options for <see cref="PlayerWalkCommandHandler"/>.
    /// </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.
        /// The command handler sleeps for this duration, then checks for new info. Unit is 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;
        public int CheckDelay { get; set; } = 50;
    }
}

D Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs +0 -58
@@ 1,58 0,0 @@
//
//  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 NosSmooth.Core.Commands;
using NosSmooth.Core.Packets;
using NosSmooth.Packets.Client.Movement;
using NosSmooth.Packets.Server.Maps;
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.PositionX, packet.Packet.PositionY);
            if (packet.Packet.PositionX == _walkStatus.TargetX && packet.Packet.PositionY == _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
}

D Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs +0 -177
@@ 1,177 0,0 @@
//
//  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.LocalBinding.Objects;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;

/// <summary>
/// The status for <see cref="WalkCommandHandler"/>.
/// </summary>
public class WalkStatus
{
    private readonly PlayerManagerBinding _playerManagerBinding;
    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="playerManagerBinding">The character binding.</param>
    public WalkStatus(PlayerManagerBinding playerManagerBinding)
    {
        _playerManagerBinding = playerManagerBinding;
        _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)
        {
            _playerManagerBinding.WalkCall += 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(ushort x, ushort y)
    {
        if (IsWalking)
        {
            if (!_userCanCancel)
            {
                return false;
            }

            CancelWalkingAsync(WalkCancelReason.UserWalked)
                .GetAwaiter()
                .GetResult();
        }
        return true;
    }
}
\ No newline at end of file

R Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkUnfinishedReason.cs +35 -42
@@ 1,43 1,36 @@
//
//  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,

    /// <summary>
    /// The nostale walk function has returned false.
    /// </summary>
    /// <remarks>
    /// The player may be stunned or immobile.
    /// </remarks>
    NosTaleReturnedFalse
//
//  WalkUnfinishedReason.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.

namespace NosSmooth.LocalClient.CommandHandlers.Walk;

/// <summary>
/// Reason for not finishing a walk.
/// </summary>
public enum WalkUnfinishedReason
{
    /// <summary>
    /// There was an unknown unfinished reason.
    /// </summary>
    Unknown,

    /// <summary>
    /// The client could not find path to the given location.
    /// </summary>
    /// <remarks>
    /// The user walked just some part of the path.
    /// </remarks>
    PathNotFound,

    /// <summary>
    /// The user has took an action that has cancelled the walk.
    /// </summary>
    UserAction,

    /// <summary>
    /// There was another walk action that cancelled this one.
    /// </summary>
    AnotherTask
}
\ No newline at end of file

M Local/NosSmooth.LocalClient/Extensions/ServiceCollectionExtensions.cs => Local/NosSmooth.LocalClient/Extensions/ServiceCollectionExtensions.cs +2 -3
@@ 28,9 28,8 @@ public static class ServiceCollectionExtensions
        serviceCollection.AddNostaleCore();
        serviceCollection.AddNostaleBindings();
        serviceCollection
            .AddCommandHandler<WalkCommandHandler>()
            .AddPacketResponder<WalkPacketResponder>()
            .AddSingleton<WalkStatus>();
            .AddTakeControlCommand()
            .AddCommandHandler<PlayerWalkCommandHandler>();
        serviceCollection.TryAddSingleton<NostaleLocalClient>();
        serviceCollection.TryAddSingleton<INostaleClient>(p => p.GetRequiredService<NostaleLocalClient>());


M Local/NosSmooth.LocalClient/NosSmooth.LocalClient.csproj => Local/NosSmooth.LocalClient/NosSmooth.LocalClient.csproj +0 -4
@@ 18,8 18,4 @@
      <PackageReference Include="Reloaded.Memory.Sigscan" Version="1.2.1" />
    </ItemGroup>

    <ItemGroup>
      <Folder Include="Bindings" />
    </ItemGroup>

</Project>

M Local/NosSmooth.LocalClient/NostaleLocalClient.cs => Local/NosSmooth.LocalClient/NostaleLocalClient.cs +31 -1
@@ 9,9 9,11 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Commands.Control;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using NosSmooth.Packets;
using NosSmooth.Packets.Errors;
using NosSmooth.PacketSerializer.Abstractions.Attributes;


@@ 29,6 31,8 @@ namespace NosSmooth.LocalClient;
public class NostaleLocalClient : BaseNostaleClient
{
    private readonly NetworkBinding _networkBinding;
    private readonly PlayerManagerBinding _playerManagerBinding;
    private readonly ControlCommands _controlCommands;
    private readonly IPacketSerializer _packetSerializer;
    private readonly IPacketHandler _packetHandler;
    private readonly ILogger _logger;


@@ 41,6 45,8 @@ public class NostaleLocalClient : BaseNostaleClient
    /// Initializes a new instance of the <see cref="NostaleLocalClient"/> class.
    /// </summary>
    /// <param name="networkBinding">The network binding.</param>
    /// <param name="playerManagerBinding">The player manager binding.</param>
    /// <param name="controlCommands">The control commands.</param>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>
    /// <param name="packetHandler">The packet handler.</param>


@@ 50,6 56,8 @@ public class NostaleLocalClient : BaseNostaleClient
    public NostaleLocalClient
    (
        NetworkBinding networkBinding,
        PlayerManagerBinding playerManagerBinding,
        ControlCommands controlCommands,
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer,
        IPacketHandler packetHandler,


@@ 61,6 69,8 @@ public class NostaleLocalClient : BaseNostaleClient
    {
        _options = options.Value;
        _networkBinding = networkBinding;
        _playerManagerBinding = playerManagerBinding;
        _controlCommands = controlCommands;
        _packetSerializer = packetSerializer;
        _packetHandler = packetHandler;
        _logger = logger;


@@ 75,6 85,9 @@ public class NostaleLocalClient : BaseNostaleClient
        _networkBinding.PacketSend += SendCallback;
        _networkBinding.PacketReceive += ReceiveCallback;

        _playerManagerBinding.FollowEntityCall += FollowEntity;
        _playerManagerBinding.WalkCall += Walk;

        try
        {
            await Task.Delay(-1, stopRequested);


@@ 86,6 99,8 @@ public class NostaleLocalClient : BaseNostaleClient

        _networkBinding.PacketSend -= SendCallback;
        _networkBinding.PacketReceive -= ReceiveCallback;
        _playerManagerBinding.FollowEntityCall -= FollowEntity;
        _playerManagerBinding.WalkCall -= Walk;

        return Result.FromSuccess();
    }


@@ 183,4 198,19 @@ public class NostaleLocalClient : BaseNostaleClient
            _logger.LogResultError(result);
        }
    }
}

    private bool FollowEntity(MapBaseObj? obj)
    {
        Task.Run
        (
            async () => await _controlCommands.CancelAsync
                (ControlCommandsFilter.UserCancellable, false, (CancellationToken)_stopRequested!)
        );
        return true;
    }

    private bool Walk(ushort x, ushort y)
    {
        return _controlCommands.AllowUserActions;
    }
}
\ No newline at end of file