From 7535dd93e52f60b543f328685a6e6333a37649eb Mon Sep 17 00:00:00 2001 From: Rutherther Date: Mon, 2 Jan 2023 00:49:36 +0100 Subject: [PATCH] feat(bindings): add periodic binding and thread synchronizer --- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../HooksConfigBuilder.cs | 26 ++++ .../NosBindingManager.cs | 59 ++++++- .../NosThreadSynchronizer.cs | 145 ++++++++++++++++++ .../Objects/PeriodicBinding.cs | 82 ++++++++++ .../Options/NosThreadSynchronizerOptions.cs | 18 +++ .../Options/PeriodicBindingOptions.cs | 21 +++ 7 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs create mode 100644 src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs create mode 100644 src/Core/NosSmooth.LocalBinding/Options/NosThreadSynchronizerOptions.cs create mode 100644 src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs diff --git a/src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs b/src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs index e430175..6f72353 100644 --- a/src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs @@ -31,12 +31,14 @@ public static class ServiceCollectionExtensions return serviceCollection .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(p => p.GetRequiredService().PlayerManager) .AddSingleton(p => p.GetRequiredService().SceneManager) .AddSingleton(p => p.GetRequiredService().PetManagerList) .AddSingleton(p => p.GetRequiredService().SceneManager) .AddSingleton(p => p.GetRequiredService().PetManagerList) .AddSingleton(p => p.GetRequiredService().PlayerManager) + .AddSingleton(p => p.GetRequiredService().Periodic) .AddSingleton(p => p.GetRequiredService().PetManager) .AddSingleton(p => p.GetRequiredService().UnitManager) .AddSingleton(p => p.GetRequiredService().Network); diff --git a/src/Core/NosSmooth.LocalBinding/HooksConfigBuilder.cs b/src/Core/NosSmooth.LocalBinding/HooksConfigBuilder.cs index 4d064e6..c69434a 100644 --- a/src/Core/NosSmooth.LocalBinding/HooksConfigBuilder.cs +++ b/src/Core/NosSmooth.LocalBinding/HooksConfigBuilder.cs @@ -34,6 +34,7 @@ public class HooksConfigBuilder private readonly HookOptionsBuilder _entityFocusHook; private readonly HookOptionsBuilder _entityFollowHook; private readonly HookOptionsBuilder _entityUnfollowHook; + private readonly HookOptionsBuilder _periodicHook; /// /// Initializes a new instance of the 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); } /// @@ -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; } @@ -92,6 +96,28 @@ public class HooksConfigBuilder return this; } + /// + /// 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. + /// + /// The configuring action. + /// This builder. + public HooksConfigBuilder HookPeriodic(Action? configure = default) + { + if (configure is not null) + { + _periodicHook.Hook(); + configure(_periodicHook); + } + else + { + _periodicHook.Hook(true); + } + + return this; + } + /// /// Configure packet send hooks. /// Enables the hook. In case you just want to change the pattern or offset diff --git a/src/Core/NosSmooth.LocalBinding/NosBindingManager.cs b/src/Core/NosSmooth.LocalBinding/NosBindingManager.cs index f77d3b0..3a43199 100644 --- a/src/Core/NosSmooth.LocalBinding/NosBindingManager.cs +++ b/src/Core/NosSmooth.LocalBinding/NosBindingManager.cs @@ -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; /// /// Initializes a new instance of the class. @@ -44,13 +46,15 @@ public class NosBindingManager : IDisposable /// The network binding options. /// The scene manager binding options. /// The pet manager binding options. + /// The periodic binding options. public NosBindingManager ( NosBrowserManager browserManager, IOptions characterBindingOptions, IOptions networkBindingOptions, IOptions sceneManagerBindingOptions, - IOptions petManagerBindingOptions + IOptions petManagerBindingOptions, + IOptions periodicBindingOptions ) { _browserManager = browserManager; @@ -61,6 +65,7 @@ public class NosBindingManager : IDisposable _networkBindingOptions = networkBindingOptions.Value; _unitManagerBindingOptions = sceneManagerBindingOptions.Value; _petManagerBindingOptions = petManagerBindingOptions.Value; + _periodicBindingOptions = periodicBindingOptions.Value; } /// @@ -78,6 +83,26 @@ public class NosBindingManager : IDisposable /// internal IMemory Memory { get; } + /// + /// Gets the periodic binding. + /// + /// Thrown if the manager is not initialized yet. + 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; + } + } + /// /// Gets the network binding. /// @@ -181,6 +206,38 @@ public class NosBindingManager : IDisposable errorResults.Add(browserInitializationResult); } + 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); diff --git a/src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs b/src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs new file mode 100644 index 0000000..e7952b1 --- /dev/null +++ b/src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs @@ -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; + +/// +/// Synchronizes with NosTale thread using a periodic function. +/// +public class NosThreadSynchronizer +{ + private readonly PeriodicBinding _periodicBinding; + private readonly NosThreadSynchronizerOptions _options; + private readonly ConcurrentQueue _queuedOperations; + + /// + /// Initializes a new instance of the class. + /// + /// The periodic function binding. + /// The options. + public NosThreadSynchronizer(PeriodicBinding periodicBinding, IOptions options) + { + _periodicBinding = periodicBinding; + _options = options.Value; + _queuedOperations = new ConcurrentQueue(); + } + + /// + /// Start the synchronizer operation. + /// + public void StartSynchronizer() + { + _periodicBinding.Periodic += Periodic; + } + + /// + /// Stop the synchronizer operation. + /// + 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 + } + } + } + + /// + /// Enqueue the given operation to execute on next frame. + /// + /// The action to execute. + public void EnqueueOperation(Action action) + { + _queuedOperations.Enqueue + ( + new SyncOperation + ( + () => + { + action(); + return Result.FromSuccess(); + }, + null + ) + ); + } + + /// + /// Synchronizes to NosTale thread, executes the given action and returns its result. + /// + /// The action to execute. + /// The cancellation token used for cancelling the operation. + /// The result of the action. + public async Task SynchronizeAsync(Func 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 action, CancellationTokenSource? CancellationTokenSource) + { + public Result? Result { get; set; } + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs b/src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs new file mode 100644 index 0000000..c1683dd --- /dev/null +++ b/src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs @@ -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; + +/// +/// Binds to a periodic function to allow synchronizing. +/// +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(); + + /// + /// Create the periodic binding with finding the periodic function. + /// + /// The binding manager. + /// The options for the binding. + /// A periodic binding or an error. + public static Result Create(NosBindingManager bindingManager, PeriodicBindingOptions options) + { + var binding = new PeriodicBinding(); + + var periodicHookResult = bindingManager.CreateCustomAsmHookFromPattern + ("PeriodicBinding.Periodic", binding.PeriodicDetour, options.PeriodicHook); + if (!periodicHookResult.IsDefined(out var periodicHook)) + { + return Result.FromError(periodicHookResult); + } + + binding._periodicHook = periodicHook; + return binding; + } + + private NosAsmHook? _periodicHook; + + private PeriodicBinding() + { + } + + /// + /// An action called on every period. + /// + public event Action? Periodic; + + /// + /// Enable all networking hooks. + /// + public void EnableHooks() + { + _periodicHook?.Hook.EnableOrActivate(); + } + + /// + /// Disable all the hooks that are currently enabled. + /// + public void DisableHooks() + { + _periodicHook?.Hook.Disable(); + } + + private void PeriodicDetour() + { + Periodic?.Invoke(); + } +} \ No newline at end of file diff --git a/src/Core/NosSmooth.LocalBinding/Options/NosThreadSynchronizerOptions.cs b/src/Core/NosSmooth.LocalBinding/Options/NosThreadSynchronizerOptions.cs new file mode 100644 index 0000000..caed641 --- /dev/null +++ b/src/Core/NosSmooth.LocalBinding/Options/NosThreadSynchronizerOptions.cs @@ -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; + +/// +/// Options for . +/// +public class NosThreadSynchronizerOptions +{ + /// + /// Gets or sets the number of max tasks per one iteration. + /// + public int MaxTasksPerIteration { get; set; } = 10; +} \ No newline at end of file diff --git a/src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs b/src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs new file mode 100644 index 0000000..63f5c92 --- /dev/null +++ b/src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs @@ -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; + +/// +/// Options for . +/// +public class PeriodicBindingOptions +{ + /// + /// Gets or sets the configuration for any periodic function hook. + /// + public HookOptions PeriodicHook { get; set; } + = new HookOptions(true, "55 8B EC 53 56 83 C4", 0); +} \ No newline at end of file -- 2.49.0