//
// 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;
}
}
}