~ruther/NosSmooth.Local

7535dd93e52f60b543f328685a6e6333a37649eb — Rutherther 2 years ago adb6da0
feat(bindings): add periodic binding and thread synchronizer
M src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs => src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs +2 -0
@@ 31,12 31,14 @@ public static class ServiceCollectionExtensions
        return serviceCollection
            .AddSingleton<NosBindingManager>()
            .AddSingleton<NosBrowserManager>()
            .AddSingleton<NosThreadSynchronizer>()
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().PlayerManager)
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().SceneManager)
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().PetManagerList)
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().SceneManager)
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().PetManagerList)
            .AddSingleton(p => p.GetRequiredService<NosBindingManager>().PlayerManager)
            .AddSingleton(p => p.GetRequiredService<NosBindingManager>().Periodic)
            .AddSingleton(p => p.GetRequiredService<NosBindingManager>().PetManager)
            .AddSingleton(p => p.GetRequiredService<NosBindingManager>().UnitManager)
            .AddSingleton(p => p.GetRequiredService<NosBindingManager>().Network);

M src/Core/NosSmooth.LocalBinding/HooksConfigBuilder.cs => src/Core/NosSmooth.LocalBinding/HooksConfigBuilder.cs +26 -0
@@ 34,6 34,7 @@ public class HooksConfigBuilder
    private readonly HookOptionsBuilder _entityFocusHook;
    private readonly HookOptionsBuilder _entityFollowHook;
    private readonly HookOptionsBuilder _entityUnfollowHook;
    private readonly HookOptionsBuilder _periodicHook;

    /// <summary>
    /// Initializes a new instance of the <see cref="HooksConfigBuilder"/> class.


@@ 47,6 48,7 @@ public class HooksConfigBuilder
        _entityFocusHook = new HookOptionsBuilder(new UnitManagerBindingOptions().EntityFocusHook);
        _entityFollowHook = new HookOptionsBuilder(new CharacterBindingOptions().EntityFollowHook);
        _entityUnfollowHook = new HookOptionsBuilder(new CharacterBindingOptions().EntityUnfollowHook);
        _periodicHook = new HookOptionsBuilder(new PeriodicBindingOptions().PeriodicHook);
    }

    /// <summary>


@@ 62,6 64,7 @@ public class HooksConfigBuilder
        _entityFocusHook.Hook(false);
        _entityFollowHook.Hook(false);
        _entityUnfollowHook.Hook(false);
        _periodicHook.Hook(false);
        return this;
    }



@@ 78,6 81,7 @@ public class HooksConfigBuilder
        _entityFocusHook.Hook(true);
        _entityFollowHook.Hook(true);
        _entityUnfollowHook.Hook(true);
        _periodicHook.Hook(true);
        return this;
    }



@@ 93,6 97,28 @@ public class HooksConfigBuilder
    }

    /// <summary>
    /// Configure periodic hook. Can be any periodic function in NosTale called on every frame.
    /// Enables the hook. In case you just want to change the pattern or offset
    /// and do not want to enable the hook, call .Hook(false) in the builder.
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookPeriodic(Action<HookOptionsBuilder>? configure = default)
    {
        if (configure is not null)
        {
            _periodicHook.Hook();
            configure(_periodicHook);
        }
        else
        {
            _periodicHook.Hook(true);
        }

        return this;
    }

    /// <summary>
    /// Configure packet send hooks.
    /// Enables the hook. In case you just want to change the pattern or offset
    /// and do not want to enable the hook, call .Hook(false) in the builder.

M src/Core/NosSmooth.LocalBinding/NosBindingManager.cs => src/Core/NosSmooth.LocalBinding/NosBindingManager.cs +58 -1
@@ 30,11 30,13 @@ public class NosBindingManager : IDisposable
    private readonly CharacterBindingOptions _characterBindingOptions;
    private readonly NetworkBindingOptions _networkBindingOptions;
    private readonly UnitManagerBindingOptions _unitManagerBindingOptions;
    private readonly PeriodicBindingOptions _periodicBindingOptions;

    private NetworkBinding? _networkBinding;
    private PlayerManagerBinding? _characterBinding;
    private UnitManagerBinding? _unitManagerBinding;
    private PetManagerBinding? _petManagerBinding;
    private PeriodicBinding? _periodicBinding;

    /// <summary>
    /// Initializes a new instance of the <see cref="NosBindingManager"/> class.


@@ 44,13 46,15 @@ public class NosBindingManager : IDisposable
    /// <param name="networkBindingOptions">The network binding options.</param>
    /// <param name="sceneManagerBindingOptions">The scene manager binding options.</param>
    /// <param name="petManagerBindingOptions">The pet manager binding options.</param>
    /// <param name="periodicBindingOptions">The periodic binding options.</param>
    public NosBindingManager
    (
        NosBrowserManager browserManager,
        IOptions<CharacterBindingOptions> characterBindingOptions,
        IOptions<NetworkBindingOptions> networkBindingOptions,
        IOptions<UnitManagerBindingOptions> sceneManagerBindingOptions,
        IOptions<PetManagerBindingOptions> petManagerBindingOptions
        IOptions<PetManagerBindingOptions> petManagerBindingOptions,
        IOptions<PeriodicBindingOptions> periodicBindingOptions
    )
    {
        _browserManager = browserManager;


@@ 61,6 65,7 @@ public class NosBindingManager : IDisposable
        _networkBindingOptions = networkBindingOptions.Value;
        _unitManagerBindingOptions = sceneManagerBindingOptions.Value;
        _petManagerBindingOptions = petManagerBindingOptions.Value;
        _periodicBindingOptions = periodicBindingOptions.Value;
    }

    /// <summary>


@@ 79,6 84,26 @@ public class NosBindingManager : IDisposable
    internal IMemory Memory { get; }

    /// <summary>
    /// Gets the periodic binding.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if the manager is not initialized yet.</exception>
    public PeriodicBinding Periodic
    {
        get
        {
            if (_periodicBinding is null)
            {
                throw new InvalidOperationException
                (
                    "Could not get periodic. The binding manager is not initialized. Did you forget to call NosBindingManager.Initialize?"
                );
            }

            return _periodicBinding;
        }
    }

    /// <summary>
    /// Gets the network binding.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if the manager is not initialized yet.</exception>


@@ 183,6 208,38 @@ public class NosBindingManager : IDisposable

        try
        {
            var periodicBinding = PeriodicBinding.Create
            (
                this,
                _periodicBindingOptions
            );
            if (!periodicBinding.IsSuccess)
            {
                errorResults.Add
                (
                    Result.FromError
                    (
                        new CouldNotInitializeModuleError(typeof(PeriodicBinding), periodicBinding.Error),
                        periodicBinding
                    )
                );
            }
            _periodicBinding = periodicBinding.Entity;
        }
        catch (Exception e)
        {
            errorResults.Add
            (
                Result.FromError
                (
                    new CouldNotInitializeModuleError(typeof(PeriodicBinding), new ExceptionError(e)),
                    (Result)new ExceptionError(e)
                )
            );
        }

        try
        {
            var network = NetworkBinding.Create(this, _networkBindingOptions);
            if (!network.IsSuccess)
            {

A src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs => src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs +145 -0
@@ 0,0 1,145 @@
//
//  NosThreadSynchronizer.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.Collections.Concurrent;
using System.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Options;
using Remora.Results;

namespace NosSmooth.LocalBinding;

/// <summary>
/// Synchronizes with NosTale thread using a periodic function.
/// </summary>
public class NosThreadSynchronizer
{
    private readonly PeriodicBinding _periodicBinding;
    private readonly NosThreadSynchronizerOptions _options;
    private readonly ConcurrentQueue<SyncOperation> _queuedOperations;

    /// <summary>
    /// Initializes a new instance of the <see cref="NosThreadSynchronizer"/> class.
    /// </summary>
    /// <param name="periodicBinding">The periodic function binding.</param>
    /// <param name="options">The options.</param>
    public NosThreadSynchronizer(PeriodicBinding periodicBinding, IOptions<NosThreadSynchronizerOptions> options)
    {
        _periodicBinding = periodicBinding;
        _options = options.Value;
        _queuedOperations = new ConcurrentQueue<SyncOperation>();
    }

    /// <summary>
    /// Start the synchronizer operation.
    /// </summary>
    public void StartSynchronizer()
    {
        _periodicBinding.Periodic += Periodic;
    }

    /// <summary>
    /// Stop the synchronizer operation.
    /// </summary>
    public void StopSynchronizer()
    {
        _periodicBinding.Periodic -= Periodic;
    }

    private void Periodic()
    {
        var tasks = _options.MaxTasksPerIteration;

        while (tasks-- > 0 && _queuedOperations.TryDequeue(out var operation))
        {
            ExecuteOperation(operation);
        }
    }

    private void ExecuteOperation(SyncOperation operation)
    {
        try
        {
            var result = operation.action();
            operation.Result = result;
        }
        catch (Exception e)
        {
            // TODO: log?
            operation.Result = e;
        }

        if (operation.CancellationTokenSource is not null)
        {
            try
            {
                operation.CancellationTokenSource.Cancel();
            }
            catch (Exception)
            {
                // ignore
            }
        }
    }

    /// <summary>
    /// Enqueue the given operation to execute on next frame.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    public void EnqueueOperation(Action action)
    {
        _queuedOperations.Enqueue
        (
            new SyncOperation
            (
                () =>
                {
                    action();
                    return Result.FromSuccess();
                },
                null
            )
        );
    }

    /// <summary>
    /// Synchronizes to NosTale thread, executes the given action and returns its result.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>The result of the action.</returns>
    public async Task<Result> SynchronizeAsync(Func<Result> action, CancellationToken ct = default)
    {
        var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
        var syncOperation = new SyncOperation(action, linkedSource);
        _queuedOperations.Enqueue(syncOperation);

        try
        {
            await Task.Delay(Timeout.Infinite, linkedSource.Token);
        }
        catch (OperationCanceledException)
        {
            if (ct.IsCancellationRequested)
            { // Throw in case the top token was cancelled.
                throw;
            }
        }
        catch (Exception e)
        {
            return new ExceptionError(e);
        }

        return syncOperation.Result ?? Result.FromSuccess();
    }

    private record SyncOperation(Func<Result> action, CancellationTokenSource? CancellationTokenSource)
    {
        public Result? Result { get; set; }
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs => src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs +82 -0
@@ 0,0 1,82 @@
//
//  PeriodicBinding.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.Diagnostics;
using NosSmooth.LocalBinding.Errors;
using NosSmooth.LocalBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using Reloaded.Hooks.Definitions.X86;
using Remora.Results;

namespace NosSmooth.LocalBinding.Objects;

/// <summary>
/// Binds to a periodic function to allow synchronizing.
/// </summary>
public class PeriodicBinding
{
    [Function
    (
        new FunctionAttribute.Register[0],
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp, FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx }
    )]
    private delegate void PeriodicDelegate();

    /// <summary>
    /// Create the periodic binding with finding the periodic function.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A periodic binding or an error.</returns>
    public static Result<PeriodicBinding> Create(NosBindingManager bindingManager, PeriodicBindingOptions options)
    {
        var binding = new PeriodicBinding();

        var periodicHookResult = bindingManager.CreateCustomAsmHookFromPattern<PeriodicDelegate>
            ("PeriodicBinding.Periodic", binding.PeriodicDetour, options.PeriodicHook);
        if (!periodicHookResult.IsDefined(out var periodicHook))
        {
            return Result<PeriodicBinding>.FromError(periodicHookResult);
        }

        binding._periodicHook = periodicHook;
        return binding;
    }

    private NosAsmHook<PeriodicDelegate>? _periodicHook;

    private PeriodicBinding()
    {
    }

    /// <summary>
    /// An action called on every period.
    /// </summary>
    public event Action? Periodic;

    /// <summary>
    /// Enable all networking hooks.
    /// </summary>
    public void EnableHooks()
    {
        _periodicHook?.Hook.EnableOrActivate();
    }

    /// <summary>
    /// Disable all the hooks that are currently enabled.
    /// </summary>
    public void DisableHooks()
    {
        _periodicHook?.Hook.Disable();
    }

    private void PeriodicDetour()
    {
        Periodic?.Invoke();
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Options/NosThreadSynchronizerOptions.cs => src/Core/NosSmooth.LocalBinding/Options/NosThreadSynchronizerOptions.cs +18 -0
@@ 0,0 1,18 @@
//
//  NosThreadSynchronizerOptions.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.LocalBinding.Options;

/// <summary>
/// Options for <see cref="NosThreadSynchronizer"/>.
/// </summary>
public class NosThreadSynchronizerOptions
{
    /// <summary>
    /// Gets or sets the number of max tasks per one iteration.
    /// </summary>
    public int MaxTasksPerIteration { get; set; } = 10;
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs => src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs +21 -0
@@ 0,0 1,21 @@
//
//  PeriodicBindingOptions.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using NosSmooth.LocalBinding.Objects;

namespace NosSmooth.LocalBinding.Options;

/// <summary>
/// Options for <see cref="PeriodicBinding"/>.
/// </summary>
public class PeriodicBindingOptions
{
    /// <summary>
    /// Gets or sets the configuration for any periodic function hook.
    /// </summary>
    public HookOptions PeriodicHook { get; set; }
        = new HookOptions(true, "55 8B EC 53 56 83 C4", 0);
}
\ No newline at end of file

Do not follow this link