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