// // NostaleLocalClient.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 NosSmooth.Core.Client; using NosSmooth.Core.Commands; using NosSmooth.Core.Commands.Control; using NosSmooth.Core.Extensions; using NosSmooth.Core.Packets; using NosSmooth.LocalBinding; using NosSmooth.LocalBinding.EventArgs; using NosSmooth.LocalBinding.Hooks; using NosSmooth.LocalBinding.Objects; using NosSmooth.LocalBinding.Structs; using NosSmooth.Packets; using NosSmooth.PacketSerializer; using NosSmooth.PacketSerializer.Abstractions.Attributes; using NosSmooth.PacketSerializer.Errors; using Remora.Results; using PacketEventArgs = NosSmooth.LocalBinding.EventArgs.PacketEventArgs; namespace NosSmooth.LocalClient; /// /// The local nostale client. /// /// /// Client used for living in the same process as NostaleClientX.exe. /// It hooks the send and receive packet methods. /// public class NostaleLocalClient : BaseNostaleClient { private readonly NosThreadSynchronizer _synchronizer; private readonly IHookManager _hookManager; private readonly ControlCommands _controlCommands; private readonly IPacketHandler _packetHandler; private readonly UserActionDetector _userActionDetector; private readonly ILogger _logger; private readonly IServiceProvider _provider; private readonly LocalClientOptions _options; private CancellationToken? _stopRequested; private IPacketInterceptor? _interceptor; /// /// Initializes a new instance of the class. /// /// The thread synchronizer. /// The hook manager. /// The control commands. /// The command processor. /// The packet handler. /// The user action detector. /// The logger. /// The options for the client. /// The dependency injection provider. public NostaleLocalClient ( NosThreadSynchronizer synchronizer, IHookManager hookManager, ControlCommands controlCommands, CommandProcessor commandProcessor, IPacketHandler packetHandler, UserActionDetector userActionDetector, ILogger logger, IOptions options, IServiceProvider provider ) : base(commandProcessor) { _options = options.Value; _synchronizer = synchronizer; _hookManager = hookManager; _controlCommands = controlCommands; _packetHandler = packetHandler; _userActionDetector = userActionDetector; _logger = logger; _provider = provider; } /// public override async Task RunAsync(CancellationToken stopRequested = default) { _stopRequested = stopRequested; _logger.LogInformation("Starting local client"); _synchronizer.StartSynchronizer(); _hookManager.PacketSend.Called += SendCallCallback; _hookManager.PacketReceive.Called += ReceiveCallCallback; _hookManager.EntityFollow.Called += FollowEntity; _hookManager.PlayerWalk.Called += Walk; _hookManager.PetWalk.Called += PetWalk; try { await Task.Delay(-1, stopRequested); } catch { // ignored } _hookManager.PacketSend.Called -= SendCallCallback; _hookManager.PacketReceive.Called -= ReceiveCallCallback; _hookManager.EntityFollow.Called -= FollowEntity; _hookManager.PlayerWalk.Called -= Walk; _hookManager.PetWalk.Called -= PetWalk; // the hooks are not needed anymore. _hookManager.DisableAll(); return Result.FromSuccess(); } /// public override async Task ReceivePacketAsync(string packetString, CancellationToken ct = default) { ReceivePacket(packetString); await ProcessPacketAsync(PacketSource.Server, packetString); return Result.FromSuccess(); } /// public override async Task SendPacketAsync(string packetString, CancellationToken ct = default) { SendPacket(packetString); await ProcessPacketAsync(PacketSource.Client, packetString); return Result.FromSuccess(); } private void ReceiveCallCallback(object? owner, PacketEventArgs packetArgs) { bool accepted = true; var packet = packetArgs.Packet; if (_options.AllowIntercept) { if (_interceptor is null) { _interceptor = _provider.GetRequiredService(); } accepted = _interceptor.InterceptReceive(ref packet); } Task.Run(async () => await ProcessPacketAsync(PacketSource.Server, packet)); if (!accepted) { packetArgs.Cancel = true; } } private void SendCallCallback(object? owner, PacketEventArgs packetArgs) { bool accepted = true; var packet = packetArgs.Packet; if (_options.AllowIntercept) { if (_interceptor is null) { _interceptor = _provider.GetRequiredService(); } accepted = _interceptor.InterceptSend(ref packet); } Task.Run(async () => await ProcessPacketAsync(PacketSource.Client, packet)); if (!accepted) { packetArgs.Cancel = true; } } private void SendPacket(string packetString) { _synchronizer.EnqueueOperation ( () => _hookManager.PacketSend.WrapperFunction(packetString) ); _logger.LogDebug($"Sending client packet {packetString}"); } private void ReceivePacket(string packetString) { _synchronizer.EnqueueOperation ( () => _hookManager.PacketReceive.WrapperFunction(packetString) ); _logger.LogDebug($"Receiving client packet {packetString}"); } private async Task ProcessPacketAsync(PacketSource type, string packetString) { try { var result = await _packetHandler.HandlePacketAsync(this, type, packetString); if (!result.IsSuccess) { _logger.LogError("There was an error whilst handling packet {packetString}", packetString); _logger.LogResultError(result); } } catch (Exception e) { _logger.LogError(e, "The process packet threw an exception"); } } private void FollowEntity(object? owner, EntityEventArgs entityEventArgs) { if (entityEventArgs.Entity is not null) { Task.Run ( async () => await _controlCommands.CancelAsync (ControlCommandsFilter.UserCancellable, false, (CancellationToken)_stopRequested!) ); } } private void PetWalk(object? owner, PetWalkEventArgs petWalkEventArgs) { if (!_userActionDetector.IsPetWalkUserOperation (petWalkEventArgs.PetManager, petWalkEventArgs.X, petWalkEventArgs.Y)) { // do not cancel operations made by NosTale or bot return; } if (_controlCommands.AllowUserActions) { Task.Run ( async () => await _controlCommands.CancelAsync (ControlCommandsFilter.UserCancellable, false, (CancellationToken)_stopRequested!) ); } else { petWalkEventArgs.Cancel = true; } } private void Walk(object? owner, WalkEventArgs walkEventArgs) { if (!_userActionDetector.IsWalkUserAction(walkEventArgs.X, walkEventArgs.Y)) { // do not cancel operations made by NosTale or bot return; } if (_controlCommands.AllowUserActions) { Task.Run ( async () => await _controlCommands.CancelAsync (ControlCommandsFilter.UserCancellable, false, (CancellationToken)_stopRequested!) ); } else { walkEventArgs.Cancel = true; } } }