~ruther/NosSmooth

3b11028d105429744f186478d4908ea6b727531e — František Boháček 3 years ago e799024 + d0ad5e7
Merge branch 'walk-command-handler' into 'main'

Walk command handler

Closes #4

See merge request Rutherther/nos-smooth!2
30 files changed, 1281 insertions(+), 86 deletions(-)

M Core/NosSmooth.Core/Commands/WalkCommand.cs
A Core/NosSmooth.Core/Packets/ParsingFailedPacket.cs
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/Errors/WalkNotFinishedError.cs
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCancelReason.cs
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandler.cs
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandlerOptions.cs
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkPacketResponder.cs
A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkStatus.cs
D Local/NosSmooth.LocalClient/CommandHandlers/WalkCommandHandler.cs
M Local/NosSmooth.LocalClient/Extensions/ServiceCollectionExtensions.cs
A Local/NosSmooth.LocalClient/Hooks/NostaleHookManager.cs
A Local/NosSmooth.LocalClient/Hooks/WalkEventArgs.cs
M Local/NosSmooth.LocalClient/LocalClientOptions.cs
M Local/NosSmooth.LocalClient/NostaleLocalClient.cs
M Local/NosSmooth.LocalCore/Character.cpp
M Local/NosSmooth.LocalCore/Character.h
A Local/NosSmooth.LocalCore/CharacterUnmanaged.cpp
A Local/NosSmooth.LocalCore/CharacterUnmanaged.h
M Local/NosSmooth.LocalCore/NetworkUnmanaged.cpp
M Local/NosSmooth.LocalCore/NosSmooth.LocalCore.vcxproj
M Local/NosSmooth.LocalCore/NosSmooth.LocalCore.vcxproj.filters
M Local/NosSmooth.LocalCore/NosSmoothCore.cpp
M NosSmooth.sln
A Samples/WalkCommands/ChatPacketInterceptor.cs
A Samples/WalkCommands/Commands/DetachCommand.cs
A Samples/WalkCommands/Commands/WalkCommands.cs
A Samples/WalkCommands/DllMain.cs
A Samples/WalkCommands/FodyWeavers.xml
A Samples/WalkCommands/Startup.cs
A Samples/WalkCommands/WalkCommands.csproj
M Core/NosSmooth.Core/Commands/WalkCommand.cs => Core/NosSmooth.Core/Commands/WalkCommand.cs +2 -1
@@ 12,4 12,5 @@ namespace NosSmooth.Core.Commands;
/// </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 WalkCommand(int TargetX, int TargetY) : ICommand;
\ No newline at end of file
/// <param name="CancelOnUserMove">Whether to cancel the walk when the user clicks to move somewhere.</param>
public record WalkCommand(int TargetX, int TargetY, bool CancelOnUserMove = true) : ICommand;
\ No newline at end of file

A Core/NosSmooth.Core/Packets/ParsingFailedPacket.cs => Core/NosSmooth.Core/Packets/ParsingFailedPacket.cs +37 -0
@@ 0,0 1,37 @@
//
//  ParsingFailedPacket.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 NosCore.Packets;
using Remora.Results;

namespace NosSmooth.Core.Packets;

/// <summary>
/// Represents packet that failed to parse correctly.
/// </summary>
public class ParsingFailedPacket : PacketBase
{
    /// <summary>
    /// Initializes a new instance of the <see cref="ParsingFailedPacket"/> class.
    /// </summary>
    /// <param name="serializerResult">The result from the serializer.</param>
    /// <param name="packet">The full text of the packet.</param>
    public ParsingFailedPacket(IResult serializerResult, string packet)
    {
        SerializerResult = serializerResult;
        Packet = packet;
    }

    /// <summary>
    /// Gets the result from the serializer.
    /// </summary>
    public IResult SerializerResult { get; }

    /// <summary>
    /// Gets he full packet string.
    /// </summary>
    public string Packet { get; }
}
\ No newline at end of file

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

A Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandler.cs => Local/NosSmooth.LocalClient/CommandHandlers/Walk/WalkCommandHandler.cs +94 -0
@@ 0,0 1,94 @@
//
//  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.LocalClient.CommandHandlers.Walk.Errors;
using NosSmoothCore;
using Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;

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

    /// <summary>
    /// Initializes a new instance of the <see cref="WalkCommandHandler"/> class.
    /// </summary>
    /// <param name="nosClient">The local client.</param>
    /// <param name="walkStatus">The walk status.</param>
    /// <param name="options">The options.</param>
    public WalkCommandHandler(NosClient nosClient, WalkStatus walkStatus, IOptions<WalkCommandHandlerOptions> options)
    {
        _options = options.Value;
        _nosClient = nosClient;
        _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)
        {
            try
            {
                _nosClient.GetCharacter().Walk(command.TargetX, command.TargetY);
            }
            catch (Exception e)
            {
                try
                {
                    await _walkStatus.CancelWalkingAsync(ct: ct);
                }
                catch
                {
                    // ignored, just for cancellation
                }

                return e;
            }

            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)
            {
                return new WalkNotFinishedError(_walkStatus.CurrentX, _walkStatus.CurrentY, WalkCancelReason.NotWalkingTooLong);
            }
        }

        return new WalkNotFinishedError(_walkStatus.CurrentX, _walkStatus.CurrentY, WalkCancelReason.Unknown);
    }
}
\ No newline at end of file

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

D Local/NosSmooth.LocalClient/CommandHandlers/WalkCommandHandler.cs => Local/NosSmooth.LocalClient/CommandHandlers/WalkCommandHandler.cs +0 -35
@@ 1,35 0,0 @@
//
//  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 NosSmooth.Core.Commands;
using NosSmoothCore;
using Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers;

/// <summary>
/// Handles <see cref="WalkCommand"/>.
/// </summary>
public class WalkCommandHandler : ICommandHandler<WalkCommand>
{
    private readonly NosClient _nosClient;

    /// <summary>
    /// Initializes a new instance of the <see cref="WalkCommandHandler"/> class.
    /// </summary>
    /// <param name="nosClient">The local client.</param>
    public WalkCommandHandler(NosClient nosClient)
    {
        _nosClient = nosClient;
    }

    /// <inheritdoc/>
    public Task<Result> HandleCommand(WalkCommand command, CancellationToken ct = default)
    {
        _nosClient.GetCharacter().Walk(command.TargetX, command.TargetY);
        return Task.Delay(1000).ContinueWith(_ => Result.FromSuccess()); // TODO: Wait for the move to finish
    }
}
\ No newline at end of file

M Local/NosSmooth.LocalClient/Extensions/ServiceCollectionExtensions.cs => Local/NosSmooth.LocalClient/Extensions/ServiceCollectionExtensions.cs +7 -1
@@ 9,6 9,8 @@ using Microsoft.Extensions.DependencyInjection.Extensions;
using NosSmooth.Core.Client;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalClient.CommandHandlers;
using NosSmooth.LocalClient.CommandHandlers.Walk;
using NosSmooth.LocalClient.Hooks;
using NosSmoothCore;

namespace NosSmooth.LocalClient.Extensions;


@@ 26,8 28,12 @@ public static class ServiceCollectionExtensions
    public static IServiceCollection AddLocalClient(this IServiceCollection serviceCollection)
    {
        serviceCollection.AddNostaleCore();
        serviceCollection.AddCommandHandler<WalkCommandHandler>();
        serviceCollection
            .AddCommandHandler<WalkCommandHandler>()
            .AddPacketResponder<WalkPacketResponder>()
            .AddSingleton<WalkStatus>();
        serviceCollection.TryAddSingleton<NostaleLocalClient>();
        serviceCollection.TryAddSingleton<NostaleHookManager>();
        serviceCollection.TryAddSingleton<NosClient>();
        serviceCollection.TryAddSingleton<INostaleClient>(p => p.GetRequiredService<NostaleLocalClient>());


A Local/NosSmooth.LocalClient/Hooks/NostaleHookManager.cs => Local/NosSmooth.LocalClient/Hooks/NostaleHookManager.cs +74 -0
@@ 0,0 1,74 @@
//
//  NostaleHookManager.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.ComponentModel;
using NosSmoothCore;
using Remora.Results;

namespace NosSmooth.LocalClient.Hooks;

/// <summary>
/// The manager for hooking functions.
/// </summary>
public class NostaleHookManager
{
    private readonly NosClient _nosClient;

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleHookManager"/> class.
    /// </summary>
    /// <param name="nosClient">The nostale client.</param>
    public NostaleHookManager(NosClient nosClient)
    {
        _nosClient = nosClient;
    }

    /// <summary>
    /// Event for the character walk function.
    /// </summary>
    public event Func<WalkEventArgs, bool>? ClientWalked;

    /// <summary>
    /// Hook the Character.Walk function.
    /// </summary>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result HookCharacterWalk()
    {
        try
        {
            _nosClient.GetCharacter().SetWalkCallback(Walk);
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

    /// <summary>
    /// Reset the registered hooks.
    /// </summary>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result ResetHooks()
    {
        try
        {
            _nosClient.ResetHooks();
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

    private bool Walk(int position)
    {
        return ClientWalked?.Invoke(new WalkEventArgs(position & 0xFFFF, (int)((position & 0xFFFF0000) >> 16))) ?? true;
    }
}
\ No newline at end of file

A Local/NosSmooth.LocalClient/Hooks/WalkEventArgs.cs => Local/NosSmooth.LocalClient/Hooks/WalkEventArgs.cs +34 -0
@@ 0,0 1,34 @@
//
//  WalkEventArgs.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.Hooks;

/// <summary>
/// The event args for event in <see cref="NostaleHookManager"/>.
/// </summary>
public class WalkEventArgs
{
    /// <summary>
    /// Initializes a new instance of the <see cref="WalkEventArgs"/> class.
    /// </summary>
    /// <param name="targetX">The target x coordinate.</param>
    /// <param name="targetY">The target y coordinate.</param>
    public WalkEventArgs(int targetX, int targetY)
    {
        TargetX = targetX;
        TargetY = targetY;
    }

    /// <summary>
    /// Gets the target x coordinate.
    /// </summary>
    public int TargetX { get; }

    /// <summary>
    /// Gets the target y coordinate.
    /// </summary>
    public int TargetY { get; }
}
\ No newline at end of file

M Local/NosSmooth.LocalClient/LocalClientOptions.cs => Local/NosSmooth.LocalClient/LocalClientOptions.cs +26 -0
@@ 4,6 4,8 @@
//  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;

/// <summary>


@@ 15,4 17,28 @@ public class LocalClientOptions
    /// Gets or sets whether the interception of packets should be allowed.
    /// </summary>
    public bool AllowIntercept { get; set; }

    /// <summary>
    /// Hook the packet sent method.
    /// </summary>
    /// <remarks>
    /// Packet handlers and interceptors won't be called for sent packets.
    /// </remarks>
    public bool HookPacketSend { get; set; } = true;

    /// <summary>
    /// Hook the packet received method.
    /// </summary>
    /// <remarks>
    /// Packet handlers and interceptors won't be called for received packets.
    /// </remarks>
    public bool HookPacketReceive { get; set; } = true;

    /// <summary>
    /// Whether to hook Character.Walk method. True by default.
    /// </summary>
    /// <remarks>
    /// If set to false, <see cref="WalkCommand.CancelOnUserMove"/> won't take any effect.
    /// </remarks>
    public bool HookCharacterWalk { get; set; } = true;
}
\ No newline at end of file

M Local/NosSmooth.LocalClient/NostaleLocalClient.cs => Local/NosSmooth.LocalClient/NostaleLocalClient.cs +35 -10
@@ 7,10 7,12 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosCore.Packets;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.LocalClient.Hooks;
using NosSmoothCore;
using Remora.Results;



@@ 26,11 28,13 @@ namespace NosSmooth.LocalClient;
public class NostaleLocalClient : BaseNostaleClient
{
    private readonly PacketSerializerProvider _packetSerializerProvider;
    private readonly NostaleHookManager _hookManager;
    private readonly IPacketHandler _packetHandler;
    private readonly ILogger _logger;
    private readonly IServiceProvider _provider;
    private readonly NosClient _client;
    private readonly LocalClientOptions _options;
    private CancellationToken? _stopRequested;
    private IPacketInterceptor? _interceptor;

    /// <summary>


@@ 39,6 43,7 @@ public class NostaleLocalClient : BaseNostaleClient
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>
    /// <param name="packetSerializerProvider">The packet serializer provider.</param>
    /// <param name="hookManager">The hooking manager.</param>
    /// <param name="packetHandler">The packet handler.</param>
    /// <param name="logger">The logger.</param>
    /// <param name="options">The options for the client.</param>


@@ 49,6 54,7 @@ public class NostaleLocalClient : BaseNostaleClient
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer,
        PacketSerializerProvider packetSerializerProvider,
        NostaleHookManager hookManager,
        IPacketHandler packetHandler,
        ILogger<NostaleLocalClient> logger,
        IOptions<LocalClientOptions> options,


@@ 59,6 65,7 @@ public class NostaleLocalClient : BaseNostaleClient
    {
        _options = options.Value;
        _packetSerializerProvider = packetSerializerProvider;
        _hookManager = hookManager;
        _packetHandler = packetHandler;
        _logger = logger;
        _provider = provider;


@@ 68,12 75,26 @@ public class NostaleLocalClient : BaseNostaleClient
    /// <inheritdoc />
    public override async Task<Result> RunAsync(CancellationToken stopRequested = default)
    {
        _stopRequested = stopRequested;
        _logger.LogInformation("Starting local client");
        NetworkCallback receiveCallback = ReceiveCallback;
        NetworkCallback sendCallback = SendCallback;

        _client.GetNetwork().SetReceiveCallback(receiveCallback);
        _client.GetNetwork().SetSendCallback(sendCallback);
        if (_options.HookPacketReceive)
        {
            _client.GetNetwork().SetReceiveCallback(receiveCallback);
        }

        if (_options.HookPacketSend)
        {
            _client.GetNetwork().SetSendCallback(sendCallback);
        }

        if (_options.HookCharacterWalk)
        {
            _hookManager.HookCharacterWalk();
        }

        _logger.LogInformation("Packet methods hooked successfully");

        try


@@ 82,6 103,7 @@ public class NostaleLocalClient : BaseNostaleClient
        }
        catch
        {
            // ignored
        }

        _client.ResetHooks();


@@ 105,6 127,7 @@ public class NostaleLocalClient : BaseNostaleClient

    private bool ReceiveCallback(string packet)
    {
        bool accepted = true;
        if (_options.AllowIntercept)
        {
            if (_interceptor is null)


@@ 112,16 135,17 @@ public class NostaleLocalClient : BaseNostaleClient
                _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
            }

            return _interceptor.InterceptReceive(ref packet);
            accepted = _interceptor.InterceptReceive(ref packet);
        }

        Task.Run(async () => await ProcessPacketAsync(PacketType.Received, packet));

        return true;
        return accepted;
    }

    private bool SendCallback(string packet)
    {
        bool accepted = true;
        if (_options.AllowIntercept)
        {
            if (_interceptor is null)


@@ 129,12 153,12 @@ public class NostaleLocalClient : BaseNostaleClient
                _interceptor = _provider.GetRequiredService<IPacketInterceptor>();
            }

            return _interceptor.InterceptSend(ref packet);
            accepted = _interceptor.InterceptSend(ref packet);
        }

        Task.Run(async () => await ProcessPacketAsync(PacketType.Sent, packet));

        return true;
        return accepted;
    }

    private void SendPacket(string packetString)


@@ 164,18 188,19 @@ public class NostaleLocalClient : BaseNostaleClient
        var packet = serializer.Deserialize(packetString);
        if (!packet.IsSuccess)
        {
            _logger.LogWarning($"Could not parse {packetString}. Reason: {packet.Error.Message}");
            return;
            _logger.LogWarning("Could not parse {Packet}. Reason:", packetString);
            _logger.LogResultError(packet);
            packet = new ParsingFailedPacket(packet, packetString);
        }

        Result result;
        if (type == PacketType.Received)
        {
            result = await _packetHandler.HandleReceivedPacketAsync(packet.Entity, packetString);
            result = await _packetHandler.HandleReceivedPacketAsync(packet.Entity, packetString, _stopRequested ?? default);
        }
        else
        {
            result = await _packetHandler.HandleSentPacketAsync(packet.Entity, packetString);
            result = await _packetHandler.HandleSentPacketAsync(packet.Entity, packetString, _stopRequested ?? default);
        }

        if (!result.IsSuccess)

M Local/NosSmooth.LocalCore/Character.cpp => Local/NosSmooth.LocalCore/Character.cpp +15 -34
@@ 1,47 1,28 @@
#include "Character.h"
#include <windows.h>
using namespace NosSmoothCore;

const BYTE WALK_OBJECT_PATTERN[] = { 0x33, 0xC9, 0x8B, 0x55, 0xFC, 0xA1, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00 };
const BYTE WALK_FUNCTION_PATTERN[] = { 0x55, 0x8B, 0xEC, 0x83, 0xC4, 0xEC, 0x53, 0x56, 0x57, 0x66, 0x89, 0x4D, 0xFA };

LPCSTR WALK_OBJECT_MASK = "xxxxxx????x????";
LPCSTR WALK_FUNCTION_MASK = "xxxxxxxxxxxxx";
using namespace System;
using namespace System::Runtime::InteropServices;

Character::Character(ModuleHook moduleHook)
{
    auto walkFunction = moduleHook.FindPattern(WALK_FUNCTION_PATTERN, WALK_FUNCTION_MASK);
    auto walkObject = *(unsigned int*)(moduleHook.FindPattern(WALK_OBJECT_PATTERN, WALK_OBJECT_MASK) + 0x6);

    if (walkFunction == 0)
    {
        throw "Could not find walk function.";
    }

    if (walkObject == 0)
    {
        throw "Could not find player object.";
    }

    _walkFunction = walkFunction;
    _playerObject = walkObject;
    CharacterUnmanaged::GetInstance()->Setup(moduleHook);
}

void CallWalk(int x, int y, unsigned int playerObject, unsigned int walkFunction)
void Character::Walk(int x, int y)
{
    DWORD position = (y << 16) | x;
    CharacterUnmanaged::GetInstance()->Walk(position);
}

    __asm
    {
        push 1
        xor ecx, ecx
        mov edx, position
        mov eax, dword ptr ds : [playerObject]
        mov eax, dword ptr ds : [eax]
        call walkFunction
    }
void Character::SetWalkCallback(WalkCallback^ walkCallback)
{
    _walkCallback = walkCallback;
    IntPtr functionPointer = Marshal::GetFunctionPointerForDelegate(walkCallback);
    CharacterUnmanaged::GetInstance()->SetWalkCallback(static_cast<NativeWalkCallback>(functionPointer.ToPointer()));
}

void Character::Walk(int x, int y)
void NosSmoothCore::Character::ResetHooks()
{
    CallWalk(x, y, _playerObject, _walkFunction);
}
\ No newline at end of file
    CharacterUnmanaged::GetInstance()->ResetHooks();
}

M Local/NosSmooth.LocalCore/Character.h => Local/NosSmooth.LocalCore/Character.h +13 -2
@@ 1,5 1,6 @@
#pragma once
#include "ModuleHook.h"
#include "CharacterUnmanaged.h"

namespace NosSmoothCore
{


@@ 18,9 19,19 @@ namespace NosSmoothCore
		/// <param name="x">The x coordinate to walk to.</param>
		/// <param name="y">The y coordinate to walk to.</param>
		void Walk(int x, int y);

		/// <summary>
		/// Registers the callback for walk function.
		/// </summary>
		/// <param name="walkCallback">The callback to call.</param>
		void SetWalkCallback(WalkCallback^ walkCallback);

		/// <summary>
		/// Reset the registered hooks.
		/// </summary>
		void ResetHooks();
	private:
		unsigned int _playerObject;
		unsigned int _walkFunction;
		WalkCallback^ _walkCallback;
	};
}


A Local/NosSmooth.LocalCore/CharacterUnmanaged.cpp => Local/NosSmooth.LocalCore/CharacterUnmanaged.cpp +119 -0
@@ 0,0 1,119 @@
#include "CharacterUnmanaged.h"
#include <detours.h>

using namespace NosSmoothCore;

const BYTE WALK_OBJECT_PATTERN[] = { 0x33, 0xC9, 0x8B, 0x55, 0xFC, 0xA1, 0x00, 0x00, 0x00, 0x00, 0xE8, 0x00, 0x00, 0x00, 0x00 };
const BYTE WALK_FUNCTION_PATTERN[] = { 0x55, 0x8B, 0xEC, 0x83, 0xC4, 0xEC, 0x53, 0x56, 0x57, 0x66, 0x89, 0x4D, 0xFA };

LPCSTR WALK_OBJECT_MASK = "xxxxxx????x????";
LPCSTR WALK_FUNCTION_MASK = "xxxxxxxxxxxxx";

void CharacterWalkDetourIn()
{
    DWORD position = 0;

    __asm
    {
        pushad
        pushfd

        mov position, edx
    }

    bool isAccepted = CharacterUnmanaged::GetInstance()->ExecuteWalkCallback(position);

    __asm
    {
        popfd
        popad
    }

    if (isAccepted) {
        CharacterUnmanaged::GetInstance()->Walk(position);
    }
}

// Detour entrypoint
// declspec naked to not mess up the stack
void __declspec(naked) CharacterWalkDetour()
{
    unsigned int returnPush;
    __asm {
        pop eax
        pop ebx
        mov returnPush, eax // we have to push this value on the stack before returning
    }

    CharacterWalkDetourIn();

    __asm {
        push returnPush
        ret
    }
}

CharacterUnmanaged::CharacterUnmanaged()
{
}

void CharacterUnmanaged::Setup(ModuleHook moduleHook)
{
    auto walkFunction = moduleHook.FindPattern(WALK_FUNCTION_PATTERN, WALK_FUNCTION_MASK);
    auto walkObject = *(unsigned int*)(moduleHook.FindPattern(WALK_OBJECT_PATTERN, WALK_OBJECT_MASK) + 0x6);

    if (walkFunction == 0)
    {
        throw "Could not find walk function.";
    }

    if (walkObject == 0)
    {
        throw "Could not find player object.";
    }

    _walkFunctionAddress = walkFunction;
    _characterObjectAddress = walkObject;
}

void CharacterUnmanaged::SetWalkCallback(NativeWalkCallback walkCallback)
{
    _walkCallback = walkCallback;
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach(&(PVOID&)_walkFunctionAddress, CharacterWalkDetour);
    DetourTransactionCommit();
}

void CharacterUnmanaged::Walk(DWORD position)
{
    unsigned int walkFunction = _walkFunctionAddress;
    unsigned int characterObject = _characterObjectAddress;

    __asm
    {
        push 1
        xor ecx, ecx
        mov edx, position
        mov eax, dword ptr ds : [characterObject]
        mov eax, dword ptr ds : [eax]
        call walkFunction
    }
}

void CharacterUnmanaged::ResetHooks()
{
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourDetach(&(PVOID&)_walkFunctionAddress, CharacterWalkDetour);
    DetourTransactionCommit();
}

bool CharacterUnmanaged::ExecuteWalkCallback(const DWORD position)
{
    if (_walkCallback != nullptr) {
        return _walkCallback(position);
    }

    return true;
}
\ No newline at end of file

A Local/NosSmooth.LocalCore/CharacterUnmanaged.h => Local/NosSmooth.LocalCore/CharacterUnmanaged.h +56 -0
@@ 0,0 1,56 @@
#pragma once
#include "ModuleHook.h"

namespace NosSmoothCore
{
	public delegate bool WalkCallback(int position);
	typedef bool(__stdcall* NativeWalkCallback)(int position);

	class CharacterUnmanaged
	{
	public:
		/// <summary>
		/// Set ups the addresses of objects.
		/// </summary>
		/// <param name="moduleHook">The hooking module holding the information about NostaleX.dat</param>
		void Setup(NosSmoothCore::ModuleHook moduleHook);

		/// <summary>
		/// Starts walking to the specified x, y position
		/// </summary>
		/// <param name="x">The coordinate to walk to.</param>
		void Walk(DWORD position);

		/// <summary>
		/// Registers the callback for walk function.
		/// </summary>
		/// <param name="walkCallback">The callback to call.</param>
		void SetWalkCallback(NativeWalkCallback walkCallback);

		/// <summary>
		/// Reset the registered hooks.
		/// </summary>
		void ResetHooks();

		/// <summary>
		/// Executes the walk callback.
		/// </summary>
		/// <param name="position">The coordinate the user wants to walk to.</param>
		/// <returns>Whether to accept the walk.</returns>
		bool ExecuteWalkCallback(const DWORD position);

		static CharacterUnmanaged* GetInstance()
		{
			static CharacterUnmanaged instance;
			return reinterpret_cast<CharacterUnmanaged*>(&instance);
		}
		unsigned int _walkFunctionAddress;
		unsigned int _characterObjectAddress;
	private:
		CharacterUnmanaged();


		NativeWalkCallback _walkCallback;
	};
}


M Local/NosSmooth.LocalCore/NetworkUnmanaged.cpp => Local/NosSmooth.LocalCore/NetworkUnmanaged.cpp +11 -3
@@ 1,6 1,8 @@
#include "NetworkUnmanaged.h"
#include <detours.h>
#include <windows.h>
#include <chrono>
#include <iostream>

using namespace NosSmoothCore;



@@ 35,7 37,7 @@ void PacketSendDetour()
        popfd
        popad
    }

    
    if (isAccepted) {
        NetworkUnmanaged::GetInstance()->SendPacket(packet);
    }


@@ 93,8 95,8 @@ void NetworkUnmanaged::Setup(ModuleHook moduleHook)

    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourAttach(&reinterpret_cast<void*&>(_sendPacketAddress), PacketSendDetour);
    DetourAttach(&(PVOID&)_receivePacketAddress, PacketReceiveDetour);
    DetourAttach(&reinterpret_cast<void*&>(_sendPacketAddress), PacketSendDetour);
    DetourTransactionCommit();
}



@@ 129,8 131,14 @@ void NetworkUnmanaged::ResetHooks()
{
    DetourTransactionBegin();
    DetourUpdateThread(GetCurrentThread());
    DetourDetach(&reinterpret_cast<void*&>(_sendPacketAddress), PacketSendDetour);
    if (_sendCallback != nullptr) {
        DetourDetach(&reinterpret_cast<void*&>(_sendPacketAddress), PacketSendDetour);
    }

    if (_receiveCallback != nullptr) {
    DetourDetach(&(PVOID&)_receivePacketAddress, PacketReceiveDetour);
    }

    DetourTransactionCommit();
}


M Local/NosSmooth.LocalCore/NosSmooth.LocalCore.vcxproj => Local/NosSmooth.LocalCore/NosSmooth.LocalCore.vcxproj +3 -0
@@ 166,6 166,7 @@
  </ItemGroup>
  <ItemGroup>
    <ClInclude Include="Character.h" />
    <ClInclude Include="CharacterUnmanaged.h" />
    <ClInclude Include="ModuleHook.h" />
    <ClInclude Include="Network.h" />
    <ClInclude Include="NetworkUnmanaged.h" />


@@ 174,12 175,14 @@
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="Character.cpp" />
    <ClCompile Include="CharacterUnmanaged.cpp" />
    <ClCompile Include="ModuleHook.cpp" />
    <ClCompile Include="Network.cpp" />
    <ClCompile Include="NetworkUnmanaged.cpp" />
    <ClCompile Include="NosSmoothCore.cpp" />
  </ItemGroup>
  <ItemGroup>
    <None Include="..\..\stylecop.json" />
    <None Include="packages.config" />
  </ItemGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />

M Local/NosSmooth.LocalCore/NosSmooth.LocalCore.vcxproj.filters => Local/NosSmooth.LocalCore/NosSmooth.LocalCore.vcxproj.filters +7 -0
@@ 33,6 33,9 @@
    <ClInclude Include="NetworkUnmanaged.h">
      <Filter>Header Files</Filter>
    </ClInclude>
    <ClInclude Include="CharacterUnmanaged.h">
      <Filter>Header Files</Filter>
    </ClInclude>
  </ItemGroup>
  <ItemGroup>
    <ClCompile Include="NosSmoothCore.cpp">


@@ 50,8 53,12 @@
    <ClCompile Include="NetworkUnmanaged.cpp">
      <Filter>Source Files</Filter>
    </ClCompile>
    <ClCompile Include="CharacterUnmanaged.cpp">
      <Filter>Source Files</Filter>
    </ClCompile>
  </ItemGroup>
  <ItemGroup>
    <None Include="packages.config" />
    <None Include="..\..\stylecop.json" />
  </ItemGroup>
</Project>
\ No newline at end of file

M Local/NosSmooth.LocalCore/NosSmoothCore.cpp => Local/NosSmooth.LocalCore/NosSmoothCore.cpp +1 -0
@@ 30,4 30,5 @@ Network^ NosClient::GetNetwork()
void NosClient::ResetHooks() 
{
	_network->ResetHooks();
	_character->ResetHooks();
}
\ No newline at end of file

M NosSmooth.sln => NosSmooth.sln +14 -0
@@ 44,6 44,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InterceptNameChanger", "Sam
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LowLevel", "LowLevel", "{9025731C-084E-4E82-8CD4-0F52D3AA1F54}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WalkCommands", "Samples\WalkCommands\WalkCommands.csproj", "{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU


@@ 196,6 198,18 @@ Global
		{F96F3AA0-131E-4B6B-AB21-BBE2DEBCEF3A}.Release|x64.Build.0 = Release|Any CPU
		{F96F3AA0-131E-4B6B-AB21-BBE2DEBCEF3A}.Release|x86.ActiveCfg = Release|Any CPU
		{F96F3AA0-131E-4B6B-AB21-BBE2DEBCEF3A}.Release|x86.Build.0 = Release|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Debug|x64.ActiveCfg = Debug|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Debug|x64.Build.0 = Debug|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Debug|x86.ActiveCfg = Debug|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Debug|x86.Build.0 = Debug|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Release|Any CPU.Build.0 = Release|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Release|x64.ActiveCfg = Release|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Release|x64.Build.0 = Release|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Release|x86.ActiveCfg = Release|Any CPU
		{18A62EF6-ADDA-4224-90AB-2D5DCFC95D3E}.Release|x86.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE

A Samples/WalkCommands/ChatPacketInterceptor.cs => Samples/WalkCommands/ChatPacketInterceptor.cs +106 -0
@@ 0,0 1,106 @@
//
//  ChatPacketInterceptor.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.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosCore.Packets.Enumerations;
using NosCore.Packets.ServerPackets.Chats;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalClient;
using Remora.Results;
using WalkCommands.Commands;

namespace WalkCommands;

/// <summary>
/// Interceptor of chat commands.
/// </summary>
public class ChatPacketInterceptor : IPacketInterceptor
{
    private readonly IServiceProvider _provider;
    private readonly ILogger<ChatPacketInterceptor> _logger;
    private readonly INostaleClient _client;

    /// <summary>
    /// Initializes a new instance of the <see cref="ChatPacketInterceptor"/> class.
    /// </summary>
    /// <param name="provider">The service provider.</param>
    /// <param name="logger">The logger.</param>
    /// <param name="client">The nostale client.</param>
    public ChatPacketInterceptor(IServiceProvider provider, ILogger<ChatPacketInterceptor> logger, INostaleClient client)
    {
        _provider = provider;
        _logger = logger;
        _client = client;
    }

    /// <inheritdoc />
    public bool InterceptSend(ref string packet)
    {
        if (packet.StartsWith($"say #"))
        {
            var packetString = packet;
            Task.Run(async () =>
            {
                try
                {
                    await ExecuteCommand(packetString.Substring(5));
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex, "Could not execute command.");
                }
            });
            return false;
        }

        return true;
    }

    /// <inheritdoc />
    public bool InterceptReceive(ref string packet)
    {
        return true;
    }

    private async Task ExecuteCommand(string command)
    {
        await _client.ReceivePacketAsync(new SayPacket
        {
            Type = SayColorType.Green, Message = $"Handling a command {command}."
        });

        var splitted = command.Split(new[] { ' ' });
        using var scope = _provider.CreateScope();
        Result result;
        switch (splitted[0])
        {
            case "walk":
                var walkCommand = scope.ServiceProvider.GetRequiredService<Commands.WalkCommands>();
                result = await walkCommand.HandleWalkToAsync(int.Parse(splitted[1]), int.Parse(splitted[2]), splitted.Length > 3 ? bool.Parse(splitted[3]) : true);
                break;
            case "detach":
                var detachCommand = scope.ServiceProvider.GetRequiredService<DetachCommand>();
                result = await detachCommand.HandleDetach();
                break;
            default:
                await _client.ReceivePacketAsync(new SayPacket
                {
                    Type = SayColorType.Red, Message = $"The command {splitted[0]} was not found."
                });
                return;
        }

        if (!result.IsSuccess)
        {
            _logger.LogError("Could not execute a command");
            _logger.LogResultError(result);
        }
    }
}
\ No newline at end of file

A Samples/WalkCommands/Commands/DetachCommand.cs => Samples/WalkCommands/Commands/DetachCommand.cs +53 -0
@@ 0,0 1,53 @@
//
//  DetachCommand.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 NosCore.Packets.Enumerations;
using NosCore.Packets.ServerPackets.Chats;
using NosSmooth.Core.Client;
using Remora.Results;

namespace WalkCommands.Commands;

/// <summary>
/// Group for detaching command that detaches the dll.
/// </summary>
public class DetachCommand
{
    private readonly CancellationTokenSource _dllStop;
    private readonly INostaleClient _client;

    /// <summary>
    /// Initializes a new instance of the <see cref="DetachCommand"/> class.
    /// </summary>
    /// <param name="dllStop">The cancellation token source to stop the client.</param>
    /// <param name="client">The nostale client.</param>
    public DetachCommand(CancellationTokenSource dllStop, INostaleClient client)
    {
        _dllStop = dllStop;
        _client = client;
    }

    /// <summary>
    /// Detach the dll.
    /// </summary>
    /// <returns>A result that may or may not have succeeded.</returns>
    public async Task<Result> HandleDetach()
    {
        var receiveResult = await _client.ReceivePacketAsync(new SayPacket
        {
            Message = "Going to detach!",
            Type = SayColorType.Green
        });

        if (!receiveResult.IsSuccess)
        {
            return receiveResult;
        }

        _dllStop.Cancel();
        return Result.FromSuccess();
    }
}
\ No newline at end of file

A Samples/WalkCommands/Commands/WalkCommands.cs => Samples/WalkCommands/Commands/WalkCommands.cs +77 -0
@@ 0,0 1,77 @@
//
//  WalkCommands.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 NosCore.Packets.Enumerations;
using NosCore.Packets.ServerPackets.Chats;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using Remora.Results;

namespace WalkCommands.Commands;

/// <summary>
/// Represents command group for walking.
/// </summary>
public class WalkCommands
{
    private readonly INostaleClient _client;

    /// <summary>
    /// Initializes a new instance of the <see cref="WalkCommands"/> class.
    /// </summary>
    /// <param name="client">The nostale client.</param>
    public WalkCommands(INostaleClient client)
    {
        _client = client ?? throw new ArgumentNullException(nameof(client));
    }

    /// <summary>
    /// Attempts to walk the character to the specified lcoation.
    /// </summary>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    /// <param name="isCancellable">Whether the user can cancel the operation.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public async Task<Result> HandleWalkToAsync
    (
        int x,
        int y,
        bool isCancellable = true,
        CancellationToken ct = default
    )
    {
        var receiveResult = await _client.ReceivePacketAsync
        (
            new SayPacket
            {
                Type = SayColorType.Red, Message = $"Going to walk to {x} {y}"
            },
            ct
        );

        if (!receiveResult.IsSuccess)
        {
            return receiveResult;
        }

        var command = new WalkCommand(x, y, isCancellable);
        var walkResult = await _client.SendCommandAsync(command, ct);
        if (!walkResult.IsSuccess)
        {
            return walkResult;
        }

        return await _client.ReceivePacketAsync
        (
            new SayPacket
            {
                Type = SayColorType.Red, Message = "Walk has finished successfully."
            },
            ct
        );
    }
}
\ No newline at end of file

A Samples/WalkCommands/DllMain.cs => Samples/WalkCommands/DllMain.cs +40 -0
@@ 0,0 1,40 @@
//
//  DllMain.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.Runtime.InteropServices;

namespace WalkCommands;

/// <summary>
/// Represents the dll entrypoint class.
/// </summary>
public class DllMain
{
    [DllImport("kernel32")]
#pragma warning disable SA1600
    public static extern bool AllocConsole();
#pragma warning restore SA1600

    /// <summary>
    /// Represents the dll entrypoint method.
    /// </summary>
    [DllExport]
    public static void Main()
    {
        AllocConsole();
        new Thread(() =>
        {
            try
            {
                new Startup().RunAsync().GetAwaiter().GetResult();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
        }).Start();
    }
}
\ No newline at end of file

A Samples/WalkCommands/FodyWeavers.xml => Samples/WalkCommands/FodyWeavers.xml +3 -0
@@ 0,0 1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <Costura />
</Weavers>
\ No newline at end of file

A Samples/WalkCommands/Startup.cs => Samples/WalkCommands/Startup.cs +51 -0
@@ 0,0 1,51 @@
//
//  Startup.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.DependencyInjection;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Client;
using NosSmooth.LocalClient;
using NosSmooth.LocalClient.Extensions;
using WalkCommands.Commands;

namespace WalkCommands;

/// <summary>
/// Startup class of WalkCommands.
/// </summary>
public class Startup
{
    private IServiceProvider BuildServices()
    {
        return new ServiceCollection()
            .AddLocalClient()
            .AddScoped<Commands.WalkCommands>()
            .AddScoped<DetachCommand>()
            .AddSingleton<CancellationTokenSource>()
            .Configure<LocalClientOptions>(o => o.AllowIntercept = true)
            .AddSingleton<IPacketInterceptor, ChatPacketInterceptor>()
            .AddLogging(b =>
            {
                b.ClearProviders();
                b.AddConsole();
                b.SetMinimumLevel(LogLevel.Debug);
            })
            .BuildServiceProvider();
    }

    /// <summary>
    /// Run the MoveToMiniland.
    /// </summary>
    /// <returns>A task that may or may not have succeeded.</returns>
    public async Task RunAsync()
    {
        var provider = BuildServices();
        var mainCancellation = provider.GetRequiredService<CancellationTokenSource>();

        var client = provider.GetRequiredService<INostaleClient>();
        await client.RunAsync(mainCancellation.Token);
    }
}
\ No newline at end of file

A Samples/WalkCommands/WalkCommands.csproj => Samples/WalkCommands/WalkCommands.csproj +76 -0
@@ 0,0 1,76 @@
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net48</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <AssemblyName>WalkCommands</AssemblyName>
    <RootNamespace>WalkCommands</RootNamespace>
    <LangVersion>10</LangVersion>
  </PropertyGroup>
  <PropertyGroup>
    <DllExportIdent>9C088A1D-54DE-4A9B-9C1B-DBC5BD5F5299</DllExportIdent>
    <DllExportMetaLibName>DllExport.dll</DllExportMetaLibName>
    <DllExportNamespace>WalkCommands</DllExportNamespace>
    <DllExportDDNSCecil>true</DllExportDDNSCecil>
    <PlatformTarget>x86</PlatformTarget>
    <DllExportOrdinalsBase>7</DllExportOrdinalsBase>
    <DllExportGenExpLib>false</DllExportGenExpLib>
    <DllExportOurILAsm>false</DllExportOurILAsm>
    <DllExportSysObjRebase>false</DllExportSysObjRebase>
    <DllExportLeaveIntermediateFiles>false</DllExportLeaveIntermediateFiles>
    <DllExportTimeout>30000</DllExportTimeout>
    <DllExportPeCheck>2</DllExportPeCheck>
    <DllExportPatches>0</DllExportPatches>
    <DllExportPreProcType>0</DllExportPreProcType>
    <DllExportPostProcType>0</DllExportPostProcType>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Costura.Fody">
      <Version>5.7.0</Version>
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="DllExport">
      <Version>1.7.4</Version>
      <Visible>false</Visible>
      <Wz>1</Wz>
    </PackageReference>
    <PackageReference Include="Fody">
      <Version>6.6.0</Version>
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection">
      <Version>6.0.0</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
      <Version>6.0.0</Version>
    </PackageReference>
    <PackageReference Include="Microsoft.Extensions.Logging.Console">
      <Version>6.0.0</Version>
    </PackageReference>
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\..\Local\NosSmooth.LocalClient\NosSmooth.LocalClient.csproj" />
  </ItemGroup>
  <ImportGroup Label=".NET DllExport">
    <Import Project="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Condition="Exists($([MSBuild]::Escape('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')))" Label="8337224c9ad9e356" />
  </ImportGroup>
  <Target Name="DllExportRestorePkg" BeforeTargets="PrepareForBuild">
    <Error Condition="!Exists('$(SolutionDir)DllExport.bat')" Text="DllExport.bat is not found. Path: '$(SolutionDir)' - https://github.com/3F/DllExport" />
    <Exec Condition="('$(DllExportModImported)' != 'true' Or !Exists('$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets')) And Exists('$(SolutionDir)DllExport.bat')" Command=".\DllExport.bat  -action Restore" WorkingDirectory="$(SolutionDir)" />
    <MSBuild Condition="'$(DllExportModImported)' != 'true'" Projects="$(SolutionDir)packages\DllExport.1.7.4\tools\net.r_eg.DllExport.targets" Targets="DllExportMetaXBaseTarget" Properties="TargetFramework=$(TargetFramework)">
      <Output TaskParameter="TargetOutputs" PropertyName="DllExportMetaXBase" />
    </MSBuild>
    <ItemGroup>
      <Reference Include="DllExport, PublicKeyToken=8337224c9ad9e356">
        <HintPath>$(SolutionDir)packages\DllExport.1.7.4\gcache\$(DllExportMetaXBase)\$(DllExportNamespace)\$(DllExportMetaLibName)</HintPath>
        <Private>False</Private>
        <SpecificVersion>False</SpecificVersion>
      </Reference>
    </ItemGroup>
  </Target>
  <Target Name="DllExportRPkgDynamicImport" BeforeTargets="PostBuildEvent" DependsOnTargets="GetFrameworkPaths" Condition="'$(DllExportModImported)' != 'true' And '$(DllExportRPkgDyn)' != 'false'">
    <MSBuild BuildInParallel="true" UseResultsCache="true" Projects="$(MSBuildProjectFullPath)" Properties="DllExportRPkgDyn=true" Targets="Build" />
  </Target>
</Project>
\ No newline at end of file

Do not follow this link