~ruther/NosSmooth.Local

be80295a2ec212f85bc4bae367fee2e45349bf6d — Rutherther 2 years ago 5014555
feat(binding): split hooks to individual classes, make a hook manager
57 files changed, 2178 insertions(+), 1547 deletions(-)

A README.md
M src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs
R src/Core/NosSmooth.LocalBinding/{HookOptions.cs => Hooks/HookOptions.cs}
R src/Core/NosSmooth.LocalBinding/{HookOptionsBuilder.cs => Hooks/HookOptionsBuilder.cs}
R src/Core/NosSmooth.LocalBinding/{HooksConfigBuilder.cs => Hooks/HooksConfigBuilder.cs}
A src/Core/NosSmooth.LocalBinding/Hooks/IEntityFocusHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IEntityFollowHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IEntityUnfollowHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IHookManager.cs
A src/Core/NosSmooth.LocalBinding/Hooks/INostaleHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IPacketReceiveHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IPacketSendHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IPeriodicHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IPetWalkHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/IPlayerWalkHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/CancelableNostaleHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityFocusHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityFollowHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityUnfollowHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/HookManager.cs
R src/Core/NosSmooth.LocalBinding/{NosAsmHook.cs => Hooks/Implementations/NosAsmHook.cs}
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PacketReceiveHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PacketSendHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PeriodicHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PetWalkHook.cs
A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PlayerWalkHook.cs
M src/Core/NosSmooth.LocalBinding/IsExternalInit.cs
M src/Core/NosSmooth.LocalBinding/NosBindingManager.cs
M src/Core/NosSmooth.LocalBinding/NosBrowserManager.cs
M src/Core/NosSmooth.LocalBinding/NosSmooth.LocalBinding.csproj
M src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs
D src/Core/NosSmooth.LocalBinding/Objects/NetworkBinding.cs
D src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs
D src/Core/NosSmooth.LocalBinding/Objects/PetManagerBinding.cs
D src/Core/NosSmooth.LocalBinding/Objects/PlayerManagerBinding.cs
D src/Core/NosSmooth.LocalBinding/Objects/UnitManagerBinding.cs
D src/Core/NosSmooth.LocalBinding/Options/CharacterBindingOptions.cs
A src/Core/NosSmooth.LocalBinding/Options/HookManagerOptions.cs
R src/Core/NosSmooth.LocalBinding/Options/{NetworkBindingOptions.cs => NetworkManagerOptions.cs}
D src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs
D src/Core/NosSmooth.LocalBinding/Options/PetManagerBindingOptions.cs
R src/Core/NosSmooth.LocalBinding/Options/{UnitManagerBindingOptions.cs => UnitManagerOptions.cs}
A src/Core/NosSmooth.LocalBinding/Structs/MapBaseList.cs
M src/Core/NosSmooth.LocalBinding/Structs/MapBaseObj.cs
A src/Core/NosSmooth.LocalBinding/Structs/NetworkManager.cs
M src/Core/NosSmooth.LocalBinding/Structs/NostaleList.cs
M src/Core/NosSmooth.LocalBinding/Structs/NostaleObject.cs
M src/Core/NosSmooth.LocalBinding/Structs/PetManagerList.cs
M src/Core/NosSmooth.LocalBinding/Structs/SceneManager.cs
A src/Core/NosSmooth.LocalBinding/Structs/UnitManager.cs
M src/Core/NosSmooth.LocalClient/CommandHandlers/Attack/AttackCommandHandler.cs
M src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PetWalkCommandHandler.cs
M src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PlayerWalkCommandHandler.cs
M src/Core/NosSmooth.LocalClient/NostaleLocalClient.cs
M src/Core/NosSmooth.LocalClient/UserActionDetector.cs
M src/Samples/External/ExternalBrowser/Program.cs
M src/Samples/HighLevel/SimplePiiBot/Commands/EntityCommands.cs
A README.md => README.md +20 -0
@@ 0,0 1,20 @@
# NosSmooth.Local
NosSmooth is a multi-platform library for NosTale
packets, data, game state and such.
See the main repository at [NosSmooth](https://github.com/Rutherther/NosSmooth).
NosSmooth.Local contains libraries for the regular NosTale client
such as memory mapping and NosSmooth client implementation for
handling the packets.

See the samples in the `src/Samples` folder.

## Features

### Bindings (hooks, memory mapping)
Hooks, options, mapping, errors

### Client
commands implementations, initialization

### Injector
copy Inject, regular injector cannot be used bcs .NET 5
\ No newline at end of file

M src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs => src/Core/NosSmooth.LocalBinding/Extensions/ServiceCollectionExtensions.cs +27 -8
@@ 5,7 5,10 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.DependencyInjection;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Hooks.Implementations;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Options;
using NosSmooth.LocalBinding.Structs;

namespace NosSmooth.LocalBinding.Extensions;


@@ 19,8 22,9 @@ public static class ServiceCollectionExtensions
    /// Adds bindings to Nostale objects along with <see cref="NosBindingManager"/> to initialize those.
    /// </summary>
    /// <remarks>
    /// Adds <see cref="PlayerManagerBinding"/> and <see cref="NetworkBinding"/>.
    /// You have to initialize these using <see cref="NosBindingManager"/>
    /// This adds <see cref="NosBindingManager"/>, <see cref="NosBrowserManager"/>,
    /// <see cref="IHookManager"/> and their siblings.
    /// You have to initialize the bindings using <see cref="NosBindingManager"/>
    /// prior to requesting them from the provider, otherwise an exception
    /// will be thrown.
    /// </remarks>


@@ 32,16 36,31 @@ public static class ServiceCollectionExtensions
            .AddSingleton<NosBindingManager>()
            .AddSingleton<NosBrowserManager>()
            .AddSingleton<NosThreadSynchronizer>()
            .AddSingleton<IHookManager, HookManager>()
            .AddSingleton<IPacketReceiveHook>(p => p.GetRequiredService<IHookManager>().PacketReceive)
            .AddSingleton<IPacketSendHook>(p => p.GetRequiredService<IHookManager>().PacketSend)
            .AddSingleton<IEntityFollowHook>(p => p.GetRequiredService<IHookManager>().EntityFollow)
            .AddSingleton<IEntityUnfollowHook>(p => p.GetRequiredService<IHookManager>().EntityUnfollow)
            .AddSingleton<IEntityFocusHook>(p => p.GetRequiredService<IHookManager>().EntityFocus)
            .AddSingleton<IPlayerWalkHook>(p => p.GetRequiredService<IHookManager>().PlayerWalk)
            .AddSingleton<IPetWalkHook>(p => p.GetRequiredService<IHookManager>().PetWalk)
            .AddSingleton<IPeriodicHook>(p => p.GetRequiredService<IHookManager>().Periodic)
            .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);
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().PlayerManager)
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().NetworkManager)
            .AddSingleton(p => p.GetRequiredService<NosBrowserManager>().UnitManager)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().PacketReceive)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().PacketSend)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().PlayerWalk)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().PetWalk)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().EntityFocus)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().EntityFollow)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().EntityUnfollow)
            .AddSingleton(p => p.GetRequiredService<IHookManager>().Periodic);
    }

    /// <summary>


@@ 52,7 71,7 @@ public static class ServiceCollectionExtensions
    /// <returns>The collection.</returns>
    public static IServiceCollection ConfigureHooks(this IServiceCollection serviceCollection, Action<HooksConfigBuilder> configure)
    {
        var builder = new HooksConfigBuilder();
        var builder = new HooksConfigBuilder(new HookManagerOptions());
        configure(builder);
        builder.Apply(serviceCollection);
        return serviceCollection;

R src/Core/NosSmooth.LocalBinding/HookOptions.cs => src/Core/NosSmooth.LocalBinding/Hooks/HookOptions.cs +24 -2
@@ 4,12 4,34 @@
//  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;
namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A configuration for hooking a function.
/// </summary>
/// <param name="Name">The name of the hook.</param>
/// <param name="Hook">Whether to hook the function.</param>
/// <param name="MemoryPattern">The memory pattern in hex. Use ?? for any bytes.</param>
/// <param name="Offset">The offset to find the function at.</param>
public record HookOptions(bool Hook, string MemoryPattern, int Offset);
\ No newline at end of file
public record HookOptions
(
    string Name,
    bool Hook,
    string MemoryPattern,
    int Offset
);

/// <summary>
/// A configuration for hooking a function.
/// </summary>
/// <param name="Name">The name of the hook.</param>
/// <param name="Hook">Whether to hook the function.</param>
/// <param name="MemoryPattern">The memory pattern in hex. Use ?? for any bytes.</param>
/// <param name="Offset">The offset to find the function at.</param>
public record HookOptions<TFunction>
(
    string Name,
    bool Hook,
    string MemoryPattern,
    int Offset
) : HookOptions(Name, Hook, MemoryPattern, Offset);
\ No newline at end of file

R src/Core/NosSmooth.LocalBinding/HookOptionsBuilder.cs => src/Core/NosSmooth.LocalBinding/Hooks/HookOptionsBuilder.cs +12 -22
@@ 4,48 4,38 @@
//  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;
namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// Builder for <see cref="HookOptions"/>.
/// </summary>
public class HookOptionsBuilder
/// <typeparam name="T">The type of hook options.</typeparam>
public class HookOptionsBuilder<T>
{
    private string _name;
    private bool _hook;
    private string _pattern;
    private int _offset;

    /// <summary>
    /// Initializes a new instance of the <see cref="HookOptionsBuilder"/> class.
    /// Initializes a new instance of the <see cref="HookOptionsBuilder{T}"/> class.
    /// </summary>
    /// <param name="options">The options.</param>
    internal HookOptionsBuilder(HookOptions options)
    internal HookOptionsBuilder(HookOptions<T> options)
    {
        _name = options.Name;
        _hook = options.Hook;
        _pattern = options.MemoryPattern;
        _offset = options.Offset;
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="HookOptionsBuilder"/> class.
    /// </summary>
    /// <param name="hook">Whether to hook the function.</param>
    /// <param name="pattern">The default pattern.</param>
    /// <param name="offset">The default offset.</param>
    internal HookOptionsBuilder(bool hook, string pattern, int offset)
    {
        _offset = offset;
        _pattern = pattern;
        _hook = hook;
    }

    /// <summary>
    /// Configure whether to hook this function.
    /// Default true.
    /// </summary>
    /// <param name="hook">Whether to hook the function.</param>
    /// <returns>This builder.</returns>
    public HookOptionsBuilder Hook(bool hook = true)
    public HookOptionsBuilder<T> Hook(bool hook = true)
    {
        _hook = hook;
        return this;


@@ 57,7 47,7 @@ public class HookOptionsBuilder
    /// </summary>
    /// <param name="pattern">The memory pattern.</param>
    /// <returns>This builder.</returns>
    public HookOptionsBuilder MemoryPattern(string pattern)
    public HookOptionsBuilder<T> MemoryPattern(string pattern)
    {
        _pattern = pattern;
        return this;


@@ 68,7 58,7 @@ public class HookOptionsBuilder
    /// </summary>
    /// <param name="offset">The offset.</param>
    /// <returns>This builder.</returns>
    public HookOptionsBuilder Offset(int offset)
    public HookOptionsBuilder<T> Offset(int offset)
    {
        _offset = offset;
        return this;


@@ 78,6 68,6 @@ public class HookOptionsBuilder
    /// Create hook options from this builder.
    /// </summary>
    /// <returns>The options.</returns>
    internal HookOptions Build()
        => new HookOptions(_hook, _pattern, _offset);
    internal HookOptions<T> Build()
        => new HookOptions<T>(_name, _hook, _pattern, _offset);
}
\ No newline at end of file

R src/Core/NosSmooth.LocalBinding/HooksConfigBuilder.cs => src/Core/NosSmooth.LocalBinding/Hooks/HooksConfigBuilder.cs +37 -56
@@ 7,7 7,7 @@
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.LocalBinding.Options;

namespace NosSmooth.LocalBinding;
namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// Provides user-friendly builder for configuring


@@ 27,28 27,29 @@ namespace NosSmooth.LocalBinding;
/// </remarks>
public class HooksConfigBuilder
{
    private readonly HookOptionsBuilder _packetSendHook;
    private readonly HookOptionsBuilder _packetReceiveHook;
    private readonly HookOptionsBuilder _playerWalkHook;
    private readonly HookOptionsBuilder _petWalkHook;
    private readonly HookOptionsBuilder _entityFocusHook;
    private readonly HookOptionsBuilder _entityFollowHook;
    private readonly HookOptionsBuilder _entityUnfollowHook;
    private readonly HookOptionsBuilder _periodicHook;
    private readonly HookOptionsBuilder<IPacketSendHook> _packetSendHook;
    private readonly HookOptionsBuilder<IPacketReceiveHook> _packetReceiveHook;
    private readonly HookOptionsBuilder<IPlayerWalkHook> _playerWalkHook;
    private readonly HookOptionsBuilder<IPetWalkHook> _petWalkHook;
    private readonly HookOptionsBuilder<IEntityFocusHook> _entityFocusHook;
    private readonly HookOptionsBuilder<IEntityFollowHook> _entityFollowHook;
    private readonly HookOptionsBuilder<IEntityUnfollowHook> _entityUnfollowHook;
    private readonly HookOptionsBuilder<IPeriodicHook> _periodicHook;

    /// <summary>
    /// Initializes a new instance of the <see cref="HooksConfigBuilder"/> class.
    /// </summary>
    internal HooksConfigBuilder()
    /// <param name="options">The default options to build from.</param>
    internal HooksConfigBuilder(HookManagerOptions options)
    {
        _playerWalkHook = new HookOptionsBuilder(new CharacterBindingOptions().WalkHook);
        _packetSendHook = new HookOptionsBuilder(new NetworkBindingOptions().PacketSendHook);
        _packetReceiveHook = new HookOptionsBuilder(new NetworkBindingOptions().PacketReceiveHook);
        _petWalkHook = new HookOptionsBuilder(new PetManagerBindingOptions().PetWalkHook);
        _entityFocusHook = new HookOptionsBuilder(new UnitManagerBindingOptions().EntityFocusHook);
        _entityFollowHook = new HookOptionsBuilder(new CharacterBindingOptions().EntityFollowHook);
        _entityUnfollowHook = new HookOptionsBuilder(new CharacterBindingOptions().EntityUnfollowHook);
        _periodicHook = new HookOptionsBuilder(new PeriodicBindingOptions().PeriodicHook);
        _packetSendHook = new HookOptionsBuilder<IPacketSendHook>(options.PacketSendHook);
        _packetReceiveHook = new HookOptionsBuilder<IPacketReceiveHook>(options.PacketReceiveHook);
        _playerWalkHook = new HookOptionsBuilder<IPlayerWalkHook>(options.PlayerWalkHook);
        _petWalkHook = new HookOptionsBuilder<IPetWalkHook>(options.PetWalkHook);
        _entityFocusHook = new HookOptionsBuilder<IEntityFocusHook>(options.EntityFocusHook);
        _entityFollowHook = new HookOptionsBuilder<IEntityFollowHook>(options.EntityFollowHook);
        _entityUnfollowHook = new HookOptionsBuilder<IEntityUnfollowHook>(options.EntityUnfollowHook);
        _periodicHook = new HookOptionsBuilder<IPeriodicHook>(options.PeriodicHook);
    }

    /// <summary>


@@ 103,7 104,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookPeriodic(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookPeriodic(Action<HookOptionsBuilder<IPeriodicHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 125,7 126,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookPacketSend(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookPacketSend(Action<HookOptionsBuilder<IPacketSendHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 147,7 148,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookPacketReceive(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookPacketReceive(Action<HookOptionsBuilder<IPacketReceiveHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 169,7 170,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookPlayerWalk(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookPlayerWalk(Action<HookOptionsBuilder<IPlayerWalkHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 191,7 192,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookPetWalk(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookPetWalk(Action<HookOptionsBuilder<IPetWalkHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 213,7 214,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookEntityFocus(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookEntityFocus(Action<HookOptionsBuilder<IEntityFocusHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 235,7 236,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookEntityFollow(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookEntityFollow(Action<HookOptionsBuilder<IEntityFollowHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 257,7 258,7 @@ public class HooksConfigBuilder
    /// </summary>
    /// <param name="configure">The configuring action.</param>
    /// <returns>This builder.</returns>
    public HooksConfigBuilder HookEntityUnfollow(Action<HookOptionsBuilder>? configure = default)
    public HooksConfigBuilder HookEntityUnfollow(Action<HookOptionsBuilder<IEntityUnfollowHook>>? configure = default)
    {
        if (configure is not null)
        {


@@ 278,38 279,18 @@ public class HooksConfigBuilder
    /// <param name="serviceCollection">The service collection.</param>
    internal void Apply(IServiceCollection serviceCollection)
    {
        serviceCollection.Configure<CharacterBindingOptions>
        serviceCollection.Configure<HookManagerOptions>
        (
            characterOptions =>
            o =>
            {
                characterOptions.WalkHook = _playerWalkHook.Build();
                characterOptions.EntityFollowHook = _entityFollowHook.Build();
                characterOptions.EntityUnfollowHook = _entityUnfollowHook.Build();
            }
        );

        serviceCollection.Configure<NetworkBindingOptions>
        (
            characterOptions =>
            {
                characterOptions.PacketReceiveHook = _packetReceiveHook.Build();
                characterOptions.PacketSendHook = _packetSendHook.Build();
            }
        );

        serviceCollection.Configure<PetManagerBindingOptions>
        (
            characterOptions =>
            {
                characterOptions.PetWalkHook = _petWalkHook.Build();
            }
        );

        serviceCollection.Configure<UnitManagerBindingOptions>
        (
            characterOptions =>
            {
                characterOptions.EntityFocusHook = _entityFocusHook.Build();
                o.PeriodicHook = _periodicHook.Build();
                o.EntityFocusHook = _entityFocusHook.Build();
                o.EntityFollowHook = _entityFollowHook.Build();
                o.EntityUnfollowHook = _entityUnfollowHook.Build();
                o.PacketSendHook = _packetSendHook.Build();
                o.PacketReceiveHook = _packetReceiveHook.Build();
                o.PetWalkHook = _petWalkHook.Build();
                o.PlayerWalkHook = _playerWalkHook.Build();
            }
        );
    }

A src/Core/NosSmooth.LocalBinding/Hooks/IEntityFocusHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IEntityFocusHook.cs +42 -0
@@ 0,0 1,42 @@
//
//  IEntityFocusHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of UnitManager.EntityFocus.
/// </summary>
public interface IEntityFocusHook : INostaleHook<IEntityFocusHook.EntityFocusDelegate,
    IEntityFocusHook.EntityFocusWrapperDelegate, EntityEventArgs>
{
    /// <summary>
    /// NosTale entity focus function to hook.
    /// </summary>
    /// <param name="unitManagerPtr">The unit manager object.</param>
    /// <param name="entityPtr">The entity object.</param>
    /// <returns>1 to proceed to NosTale function, 0 to block the call.</returns>
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate nuint EntityFocusDelegate(nuint unitManagerPtr, nuint entityPtr);

    /// <summary>
    /// Entity focus function.
    /// </summary>
    /// <remarks>
    /// In case entity is null, unfocus any entity.
    /// </remarks>
    /// <param name="entity">The entity object.</param>
    public delegate void EntityFocusWrapperDelegate(MapBaseObj? entity);
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IEntityFollowHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IEntityFollowHook.cs +50 -0
@@ 0,0 1,50 @@
//
//  IEntityFollowHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of CharacterManager.EntityFollow.
/// </summary>
public interface IEntityFollowHook : INostaleHook<IEntityFollowHook.EntityFollowDelegate,
    IEntityFollowHook.EntityFollowWrapperDelegate, EntityEventArgs>
{
    /// <summary>
    /// NosTale entity follow function to hook.
    /// </summary>
    /// <param name="playerManagerPtr">The player manager object.</param>
    /// <param name="entityPtr">The entity object.</param>
    /// <param name="unknown1">Unknown 1. TODO.</param>
    /// <param name="unknown2">Unknown 2. TODO.</param>
    /// <returns>1 to proceed to NosTale function, 0 to block the call.</returns>
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate nuint EntityFollowDelegate
    (
        nuint playerManagerPtr,
        nuint entityPtr,
        int unknown1 = 0,
        int unknown2 = 1
    );

    /// <summary>
    /// Entity follow function.
    /// </summary>
    /// <remarks>
    /// Does not support unfollow. For unfollow, use <see cref="IEntityUnfollowHook"/>.
    /// </remarks>
    /// <param name="entity">The entity object.</param>
    public delegate void EntityFollowWrapperDelegate(MapBaseObj entity);
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IEntityUnfollowHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IEntityUnfollowHook.cs +37 -0
@@ 0,0 1,37 @@
//
//  IEntityUnfollowHook.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.EventArgs;
using Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of CharacterManager.EntityFollow.
/// </summary>
public interface IEntityUnfollowHook : INostaleHook<IEntityUnfollowHook.EntityUnfollowDelegate,
    IEntityUnfollowHook.EntityUnfollowWrapperDelegate, EntityEventArgs>
{
    /// <summary>
    /// NosTale entity follow function to hook.
    /// </summary>
    /// <param name="playerManagerPtr">The player manager object.</param>
    /// <param name="unknown">Unknown. TODO.</param>
    /// <returns>1 to proceed to NosTale function, 0 to block the call.</returns>
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate nuint EntityUnfollowDelegate(nuint playerManagerPtr, int unknown = 0);

    /// <summary>
    /// Entity unfollow function.
    /// </summary>
    public delegate void EntityUnfollowWrapperDelegate();
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IHookManager.cs => src/Core/NosSmooth.LocalBinding/Hooks/IHookManager.cs +132 -0
@@ 0,0 1,132 @@
//
//  IHookManager.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.LocalBinding.Hooks;

/// <summary>
/// A manager holding all NosTale hooks with actions to execute on all of them.
/// </summary>
public interface IHookManager
{
    /// <summary>
    /// A name of packet send hook.
    /// </summary>
    public const string PacketSendName = "NetworkManager.PacketSend";

    /// <summary>
    /// A name of packet receive hook.
    /// </summary>
    public const string PacketReceiveName = "NetworkManager.PacketReceive";

    /// <summary>
    /// A name of character walk hook.
    /// </summary>
    public const string CharacterWalkName = "CharacterManager.Walk";

    /// <summary>
    /// A name of pet walk hook.
    /// </summary>
    public const string PetWalkName = "PetManager.Walk";

    /// <summary>
    /// A name of entity follow hook.
    /// </summary>
    public const string EntityFollowName = "CharacterManager.EntityFollow";

    /// <summary>
    /// A name of entity unfollow hook.
    /// </summary>
    public const string EntityUnfollowName = "CharacterManager.EntityUnfollow";

    /// <summary>
    /// A name of entity focus hook.
    /// </summary>
    public const string EntityFocusName = "UnitManager.EntityFocus";

    /// <summary>
    /// A name of periodic hook.
    /// </summary>
    public const string PeriodicName = "Periodic";

    /// <summary>
    /// Gets the packet send hook.
    /// </summary>
    public IPacketSendHook PacketSend { get; }

    /// <summary>
    /// Gets the packet receive hook.
    /// </summary>
    public IPacketReceiveHook PacketReceive { get; }

    /// <summary>
    /// Gets the player walk hook.
    /// </summary>
    public IPlayerWalkHook PlayerWalk { get; }

    /// <summary>
    /// Gets the entity follow hook.
    /// </summary>
    public IEntityFollowHook EntityFollow { get; }

    /// <summary>
    /// Gets the entity unfollow hook.
    /// </summary>
    public IEntityUnfollowHook EntityUnfollow { get; }

    /// <summary>
    /// Gets the player walk hook.
    /// </summary>
    public IPetWalkHook PetWalk { get; }

    /// <summary>
    /// Gets the entity focus hook.
    /// </summary>
    public IEntityFocusHook EntityFocus { get; }

    /// <summary>
    /// Gets the periodic function hook.
    /// </summary>
    /// <remarks>
    /// May be any function that is called periodically.
    /// This is used for synchronizing using <see cref="NosThreadSynchronizer"/>.
    /// </remarks>
    public IPeriodicHook Periodic { get; }

    /// <summary>
    /// Gets all of the hooks.
    /// </summary>
    public IReadOnlyList<INostaleHook> Hooks { get; }

    /// <summary>
    /// Initializes all hooks.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="browserManager">The browser manager.</param>
    /// <returns>A result that may or may not have failed.</returns>
    public IResult Initialize(NosBindingManager bindingManager, NosBrowserManager browserManager);

    /// <summary>
    /// Enable hooks from the given list.
    /// </summary>
    /// <remarks>
    /// Use constants from <see cref="IHookManager"/>,
    /// such as IHookManager.PacketSendName.
    /// </remarks>
    /// <param name="names">The hooks to enable.</param>
    public void Enable(IEnumerable<string> names);

    /// <summary>
    /// Disable all hooks.
    /// </summary>
    public void DisableAll();

    /// <summary>
    /// Enable all hooks.
    /// </summary>
    public void EnableAll();
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/INostaleHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/INostaleHook.cs +74 -0
@@ 0,0 1,74 @@
//
//  INostaleHook.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.LocalBinding.Hooks;

/// <summary>
/// A hook of a NosTale function.
/// </summary>
/// <typeparam name="TFunction">The function delegate.</typeparam>
/// <typeparam name="TWrapperFunction">A wrapper function that abstracts the call to original function. May get the neccessary object to call the function and accept only relevant arguments.</typeparam>
/// <typeparam name="TEventArgs">The event args used in case of a call.</typeparam>
public interface INostaleHook<TFunction, TWrapperFunction, TEventArgs> : INostaleHook
    where TFunction : Delegate
    where TWrapperFunction : Delegate
    where TEventArgs : System.EventArgs
{
    /// <summary>
    /// Gets the wrapper function delegate.
    /// </summary>
    public TWrapperFunction WrapperFunction { get; }

    /// <summary>
    /// Gets the original function delegate.
    /// </summary>
    public TFunction OriginalFunction { get; }

    /// <summary>
    /// An event fired in case the function is called from NosTale.
    /// </summary>
    public event EventHandler<TEventArgs>? Called;
}

/// <summary>
/// A hook of a NosTale function.
/// </summary>
public interface INostaleHook
{
    /// <summary>
    /// Gets the name of the function.
    /// </summary>
    /// <remarks>
    /// Usually denoted as Type.Method,
    /// for example PlayerManager.Walk.
    /// </remarks>
    public string Name { get; }

    /// <summary>
    /// Gets whether the hook is currently enabled.
    /// </summary>
    public bool IsEnabled { get; }

    /// <summary>
    /// Enable the hook.
    /// </summary>
    /// <remarks>
    /// If it already is enabled, does nothing.
    /// </remarks>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Enable();

    /// <summary>
    /// Disable the hook.
    /// </summary>
    /// <remarks>
    /// If it already is disabled, does nothing.
    /// </remarks>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result Disable();
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IPacketReceiveHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IPacketReceiveHook.cs +39 -0
@@ 0,0 1,39 @@
//
//  IPacketReceiveHook.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.EventArgs;
using NosSmooth.LocalBinding.Objects;
using Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of NetworkManager.PacketSend.
/// </summary>
public interface IPacketReceiveHook : INostaleHook<IPacketReceiveHook.PacketReceiveDelegate,
    IPacketReceiveHook.PacketReceiveWrapperDelegate, PacketEventArgs>
{
    /// <summary>
    /// NosTale packet send function to hook.
    /// </summary>
    /// <param name="packetObject">The packet object.</param>
    /// <param name="packetString">Pointer to <see cref="NostaleStringA"/>.Get() object.</param>
    /// <returns>1 to proceed to NosTale function, 0 to block the call.</returns>
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate nuint PacketReceiveDelegate(nuint packetObject, nuint packetString);

    /// <summary>
    /// Packet send function.
    /// </summary>
    /// <param name="packetString">The string to send.</param>
    public delegate void PacketReceiveWrapperDelegate(string packetString);
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IPacketSendHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IPacketSendHook.cs +38 -0
@@ 0,0 1,38 @@
//
//  IPacketSendHook.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.EventArgs;
using NosSmooth.LocalBinding.Objects;
using Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of NetworkManager.PacketSend.
/// </summary>
public interface IPacketSendHook : INostaleHook<IPacketSendHook.PacketSendDelegate, IPacketSendHook.PacketSendWrapperDelegate, PacketEventArgs>
{
    /// <summary>
    /// NosTale packet send function to hook.
    /// </summary>
    /// <param name="packetObject">The packet object.</param>
    /// <param name="packetString">Pointer to <see cref="NostaleStringA"/>.Get() object.</param>
    /// <returns>1 to proceed to NosTale function, 0 to block the call.</returns>
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate nuint PacketSendDelegate(nuint packetObject, nuint packetString);

    /// <summary>
    /// Packet send function.
    /// </summary>
    /// <param name="packetString">The string to send.</param>
    public delegate void PacketSendWrapperDelegate(string packetString);
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IPeriodicHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IPeriodicHook.cs +29 -0
@@ 0,0 1,29 @@
//
//  IPeriodicHook.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 Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of a periodic function,
/// preferably called every frame.
/// </summary>
public interface
    IPeriodicHook : INostaleHook<IPeriodicHook.PeriodicDelegate, IPeriodicHook.PeriodicDelegate, System.EventArgs>
{
    /// <summary>
    /// NosTale periodic function to hook.
    /// </summary>
    [Function
    (
        new FunctionAttribute.Register[0],
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate void PeriodicDelegate();
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IPetWalkHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IPetWalkHook.cs +43 -0
@@ 0,0 1,43 @@
//
//  IPetWalkHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of PetManager.Walk.
/// </summary>
public interface IPetWalkHook : INostaleHook<IPetWalkHook.PetWalkDelegate, IPetWalkHook.PetWalkWrapperDelegate, PetWalkEventArgs>
{
    /// <summary>
    /// NosTale walk function to hook.
    /// </summary>
    /// <param name="petManagerPtr">The pointer to a pet manager object.</param>
    /// <param name="position">The position to walk to. First 4 bits are x (most significant), next 4 bits are y.</param>
    /// <param name="unknown0">Unknown 1. TODO.</param>
    /// <param name="unknown1">Unknown 2. TODO.</param>
    /// <returns>1 to proceed to NosTale function, 0 to block the call.</returns>
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate nuint PetWalkDelegate(nuint petManagerPtr, int position, short unknown0 = 0, int unknown1 = 1);

    /// <summary>
    /// Pet walk function.
    /// </summary>
    /// <param name="manager">The pet manager.</param>
    /// <param name="x">The x coordinate to walk to.</param>
    /// <param name="y">The y coordinate to walk to.</param>
    /// <returns>A bool signaling whether the operation was successful.</returns>
    public delegate bool PetWalkWrapperDelegate(PetManager manager, ushort x, ushort y);
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/IPlayerWalkHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/IPlayerWalkHook.cs +41 -0
@@ 0,0 1,41 @@
//
//  IPlayerWalkHook.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.EventArgs;
using Reloaded.Hooks.Definitions.X86;

namespace NosSmooth.LocalBinding.Hooks;

/// <summary>
/// A hook of CharacterManager.Walk.
/// </summary>
public interface IPlayerWalkHook : INostaleHook<IPlayerWalkHook.WalkDelegate, IPlayerWalkHook.WalkWrapperDelegate, WalkEventArgs>
{
    /// <summary>
    /// NosTale walk function to hook.
    /// </summary>
    /// <param name="playerManagerPtr">The pointer to a player manager object.</param>
    /// <param name="position">The position to walk to. First 4 bits are x (most significant), next 4 bits are y.</param>
    /// <param name="unknown0">Unknown 1. TODO.</param>
    /// <param name="unknown1">Unknown 2. TODO.</param>
    /// <returns>1 to proceed to NosTale function, 0 to block the call.</returns>
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    public delegate nuint WalkDelegate(nuint playerManagerPtr, int position, short unknown0 = 0, int unknown1 = 1);

    /// <summary>
    /// Player walk function.
    /// </summary>
    /// <param name="x">The x coordinate to go to.</param>
    /// <param name="y">The y coordinate to go to.</param>
    /// <returns>A bool signaling whether the operation was successful.</returns>
    public delegate bool WalkWrapperDelegate(ushort x, ushort y);
}

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/CancelableNostaleHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/CancelableNostaleHook.cs +135 -0
@@ 0,0 1,135 @@
//
//  CancelableNostaleHook.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 System.Diagnostics;
using NosSmooth.LocalBinding.Extensions;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of a NosTale function
/// that may be cancelled (not propagated to NosTale) when fired.
/// </summary>
/// <typeparam name="TFunction">The function delegate.</typeparam>
/// <typeparam name="TWrapperFunction">A wrapper function that abstracts the call to original function. May get the neccessary object to call the function and accept only relevant arguments.</typeparam>
/// <typeparam name="TEventArgs">The event args used in case of a call.</typeparam>
public abstract class CancelableNostaleHook<TFunction, TWrapperFunction, TEventArgs> : INostaleHook<TFunction, TWrapperFunction, TEventArgs>
    where TFunction : Delegate
    where TWrapperFunction : Delegate
    where TEventArgs : CancelEventArgs
{
    /// <summary>
    /// Creates the hook instance.
    /// </summary>
    /// <param name="bindingManager">The binding manager to create the hook.</param>
    /// <param name="new">Create new hook object.</param>
    /// <param name="detour">The function that obtains detour.</param>
    /// <param name="options">The options.</param>
    /// <typeparam name="T">The hook type.</typeparam>
    /// <returns>The hook, or failed result.</returns>
    protected static Result<T> CreateHook<T>(NosBindingManager bindingManager, Func<T> @new, Func<T, TFunction> detour, HookOptions options)
        where T : CancelableNostaleHook<TFunction, TWrapperFunction, TEventArgs>
    {
        var nosHook = @new();

        var hookResult = bindingManager.CreateCustomAsmHookFromPattern<TFunction>
            (nosHook.Name, detour(nosHook), options);
        if (!hookResult.IsDefined(out var hook))
        {
            return Result<T>.FromError(hookResult);
        }

        nosHook.Hook = hook;
        return nosHook;
    }

    private TFunction? _originalFunction;

    /// <summary>
    /// Gets the hook.
    /// </summary>
    protected NosAsmHook<TFunction> Hook { get; set; } = null!;

    /// <summary>
    /// Set to true at start of WrapWithCalling, set to false at end of WrapWithCalling.
    /// </summary>
    protected bool CallingFromNosSmooth { get; set; }

    /// <inheritdoc />
    public bool IsEnabled => Hook.Hook.IsEnabled;

    /// <inheritdoc />
    public abstract TWrapperFunction WrapperFunction { get; }

    /// <inheritdoc/>
    public TFunction OriginalFunction
    {
        get
        {
            if (_originalFunction is null)
            {
                _originalFunction = WrapWithCalling(Hook.OriginalFunction.GetWrapper());
            }

            return _originalFunction;
        }
    }

    /// <inheritdoc />
    public event EventHandler<TEventArgs>? Called;

    /// <inheritdoc />
    public abstract string Name { get; }

    /// <inheritdoc />
    public Result Enable()
    {
        Hook.Hook.EnableOrActivate();
        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public Result Disable()
    {
        Hook.Hook.Disable();
        return Result.FromSuccess();
    }

    /// <summary>
    /// Wrap the target function with setting CallingFromNosSmooth to true.
    /// </summary>
    /// <example>
    /// protected MyFun WrapWithCalling(MyFun fun) {
    ///     return (a, b) => {
    ///       CallingFromNosSmooth = true;
    ///       var res = fun(a, b);
    ///       CallingFromNosSmooth = false;
    ///       return res;
    ///     }
    /// }.
    /// </example>
    /// <param name="function">The function to wrap.</param>
    /// <returns>The wrapped function.</returns>
    protected abstract TFunction WrapWithCalling(TFunction function);

    /// <summary>
    /// Calls the event, returns whether to proceed.
    /// </summary>
    /// <param name="args">The event arguments.</param>
    /// <returns>Whether to proceed.</returns>
    protected nuint HandleCall(TEventArgs args)
    {
        if (CallingFromNosSmooth)
        { // this is a call from NosSmooth, do not invoke the event.
            return 1;
        }

        Called?.Invoke(this, args);
        return args.Cancel ? 0 : (nuint)1;
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityFocusHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityFocusHook.cs +75 -0
@@ 0,0 1,75 @@
//
//  EntityFocusHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of NetworkManager.EntityFocus.
/// </summary>
internal class EntityFocusHook : CancelableNostaleHook<IEntityFocusHook.EntityFocusDelegate,
    IEntityFocusHook.EntityFocusWrapperDelegate, EntityEventArgs>, IEntityFocusHook
{
    /// <summary>
    /// Create the packet send hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<EntityFocusHook> Create
        (NosBindingManager bindingManager, NosBrowserManager browserManager, HookOptions<IEntityFocusHook> options)
    {
        var hook = CreateHook
        (
            bindingManager,
            () => new EntityFocusHook(browserManager.UnitManager),
            hook => hook.Detour,
            options
        );

        return hook;
    }

    private readonly UnitManager _unitManager;

    private EntityFocusHook(UnitManager unitManager)
    {
        _unitManager = unitManager;
    }

    /// <inheritdoc />
    public override string Name => IHookManager.EntityFocusName;

    /// <inheritdoc />
    public override IEntityFocusHook.EntityFocusWrapperDelegate WrapperFunction => (entity) => OriginalFunction
        (_unitManager.Address, entity?.Address ?? 0);

    /// <inheritdoc />
    protected override IEntityFocusHook.EntityFocusDelegate WrapWithCalling(IEntityFocusHook.EntityFocusDelegate function)
        => (unitManagerPtr, entityPtr) =>
            {
                CallingFromNosSmooth = true;
                var res = function(unitManagerPtr, entityPtr);
                CallingFromNosSmooth = false;
                return res;
            };

    private nuint Detour
    (
        nuint unitManagerPtr,
        nuint entityPtr
    )
    {
        var entity = new MapBaseObj(_unitManager.Memory, entityPtr);
        var entityArgs = new EntityEventArgs(entity);
        return HandleCall(entityArgs);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityFollowHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityFollowHook.cs +76 -0
@@ 0,0 1,76 @@
//
//  EntityFollowHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of NetworkManager.EntityFollow.
/// </summary>
internal class EntityFollowHook : CancelableNostaleHook<IEntityFollowHook.EntityFollowDelegate,
    IEntityFollowHook.EntityFollowWrapperDelegate, EntityEventArgs>, IEntityFollowHook
{
    /// <summary>
    /// Create the packet send hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<EntityFollowHook> Create
        (NosBindingManager bindingManager, NosBrowserManager browserManager, HookOptions<IEntityFollowHook> options)
    {
        var hook = CreateHook
        (
            bindingManager,
            () => new EntityFollowHook(browserManager.PlayerManager),
            hook => hook.Detour,
            options
        );

        return hook;
    }

    private readonly PlayerManager _playerManager;

    private EntityFollowHook(PlayerManager playerManager)
    {
        _playerManager = playerManager;
    }

    /// <inheritdoc />
    public override string Name => IHookManager.EntityFollowName;

    /// <inheritdoc />
    public override IEntityFollowHook.EntityFollowWrapperDelegate WrapperFunction => (entity) => OriginalFunction
        (_playerManager.Address, entity?.Address ?? 0);

    /// <inheritdoc />
    protected override IEntityFollowHook.EntityFollowDelegate WrapWithCalling(IEntityFollowHook.EntityFollowDelegate function)
        => (playerManagerPtr, entityPtr, un0, un1) =>
            {
                CallingFromNosSmooth = true;
                var res = function(playerManagerPtr, entityPtr, un0, un1);
                CallingFromNosSmooth = false;
                return res;
            };

    private nuint Detour
    (
        nuint playerManagerPtr,
        nuint entityPtr,
        int unknown1 = 0,
        int unknown2 = 1
    )
    {
        var entity = new MapBaseObj(_playerManager.Memory, entityPtr);
        var entityArgs = new EntityEventArgs(entity);
        return HandleCall(entityArgs);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityUnfollowHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/EntityUnfollowHook.cs +70 -0
@@ 0,0 1,70 @@
//
//  EntityUnfollowHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of NetworkManager.EntityUnfollow.
/// </summary>
internal class EntityUnfollowHook : CancelableNostaleHook<IEntityUnfollowHook.EntityUnfollowDelegate,
    IEntityUnfollowHook.EntityUnfollowWrapperDelegate, EntityEventArgs>, IEntityUnfollowHook
{
    /// <summary>
    /// Create the packet send hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<EntityUnfollowHook> Create
        (NosBindingManager bindingManager, NosBrowserManager browserManager, HookOptions<IEntityUnfollowHook> options)
    {
        var hook = CreateHook
        (
            bindingManager,
            () => new EntityUnfollowHook(browserManager.PlayerManager),
            hook => hook.Detour,
            options
        );

        return hook;
    }

    private readonly PlayerManager _playerManager;

    private EntityUnfollowHook(PlayerManager playerManager)
    {
        _playerManager = playerManager;
    }

    /// <inheritdoc />
    public override string Name => IHookManager.EntityUnfollowName;

    /// <inheritdoc />
    public override IEntityUnfollowHook.EntityUnfollowWrapperDelegate WrapperFunction
        => () => OriginalFunction(_playerManager.Address);

    /// <inheritdoc />
    protected override IEntityUnfollowHook.EntityUnfollowDelegate WrapWithCalling(IEntityUnfollowHook.EntityUnfollowDelegate function)
        => (playerManagerPtr, un) =>
            {
                CallingFromNosSmooth = true;
                var res = function(playerManagerPtr, un);
                CallingFromNosSmooth = false;
                return res;
            };

    private nuint Detour
    (
        nuint playerManagerPtr,
        int unknown = 0
    )
        => HandleCall(new EntityEventArgs(null));
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/HookManager.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/HookManager.cs +143 -0
@@ 0,0 1,143 @@
//
//  HookManager.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 Microsoft.Extensions.Options;
using NosSmooth.LocalBinding.Options;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <inheritdoc />
internal class HookManager : IHookManager
{
    private readonly HookManagerOptions _options;
    private List<INostaleHook> _hooks;

    /// <summary>
    /// Initializes a new instance of the <see cref="HookManager"/> class.
    /// </summary>
    /// <param name="options">The hook manager options.</param>
    public HookManager(IOptions<HookManagerOptions> options)
    {
        _options = options.Value;
        _hooks = new List<INostaleHook>();
    }

    /// <inheritdoc/>
    public IPacketSendHook PacketSend => GetHook<IPacketSendHook>(IHookManager.PacketSendName);

    /// <inheritdoc/>
    public IPacketReceiveHook PacketReceive => GetHook<IPacketReceiveHook>(IHookManager.PacketReceiveName);

    /// <inheritdoc/>
    public IPlayerWalkHook PlayerWalk => GetHook<IPlayerWalkHook>(IHookManager.CharacterWalkName);

    /// <inheritdoc/>
    public IEntityFollowHook EntityFollow => GetHook<IEntityFollowHook>(IHookManager.EntityFollowName);

    /// <inheritdoc/>
    public IEntityUnfollowHook EntityUnfollow => GetHook<IEntityUnfollowHook>(IHookManager.EntityUnfollowName);

    /// <inheritdoc/>
    public IPetWalkHook PetWalk => GetHook<IPetWalkHook>(IHookManager.PetWalkName);

    /// <inheritdoc/>
    public IEntityFocusHook EntityFocus => GetHook<IEntityFocusHook>(IHookManager.EntityFocusName);

    /// <inheritdoc/>
    public IPeriodicHook Periodic => GetHook<IPeriodicHook>(IHookManager.PeriodicName);

    /// <inheritdoc/>
    public IReadOnlyList<INostaleHook> Hooks => _hooks.AsReadOnly();

    /// <inheritdoc/>
    public IResult Initialize(NosBindingManager bindingManager, NosBrowserManager browserManager)
    {
        return HandleResults
        (
            PeriodicHook.Create(bindingManager, _options.PeriodicHook).Map(MapHook),
            EntityFocusHook.Create(bindingManager, browserManager, _options.EntityFocusHook).Map(MapHook),
            EntityFollowHook.Create(bindingManager, browserManager, _options.EntityFollowHook).Map(MapHook),
            EntityUnfollowHook.Create(bindingManager, browserManager, _options.EntityUnfollowHook).Map(MapHook),
            PlayerWalkHook.Create(bindingManager, browserManager, _options.PlayerWalkHook).Map(MapHook),
            PetWalkHook.Create(bindingManager, _options.PetWalkHook).Map(MapHook),
            PacketSendHook.Create(bindingManager, browserManager, _options.PacketSendHook).Map(MapHook),
            PacketReceiveHook.Create(bindingManager, browserManager, _options.PacketReceiveHook).Map(MapHook)
        );
    }

    private INostaleHook MapHook<T>(T original)
        where T : INostaleHook
    {
        return original;
    }

    private IResult HandleResults(params Result<INostaleHook>[] results)
    {
        List<IResult> errorResults = new List<IResult>();
        foreach (var result in results)
        {
            if (result.IsSuccess)
            {
                _hooks.Add(result.Entity);
            }
            else
            {
                errorResults.Add(result);
            }
        }

        return errorResults.Count switch
        {
            0 => Result.FromSuccess(),
            1 => errorResults[0],
            _ => (Result)new AggregateError(errorResults)
        };
    }

    /// <inheritdoc/>
    public void Enable(IEnumerable<string> names)
    {
        foreach (var hook in Hooks
            .Where(x => names.Contains(x.Name)))
        {
            hook.Enable();
        }
    }

    /// <inheritdoc/>
    public void DisableAll()
    {
        foreach (var hook in Hooks)
        {
            hook.Disable();
        }
    }

    /// <inheritdoc/>
    public void EnableAll()
    {
        foreach (var hook in Hooks)
        {
            hook.Enable();
        }
    }

    private T GetHook<T>(string name)
        where T : INostaleHook
    {
        var hook = _hooks.FirstOrDefault(x => x.Name == name);

        if (hook is null || hook is not T typed)
        {
            throw new InvalidOperationException
                ($"Could not load hook {name}. Did you forget to call IHookManager.Initialize?");
        }

        return typed;
    }
}
\ No newline at end of file

R src/Core/NosSmooth.LocalBinding/NosAsmHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/NosAsmHook.cs +1 -1
@@ 6,7 6,7 @@

using Reloaded.Hooks.Definitions;

namespace NosSmooth.LocalBinding;
namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// An assembly hook data.

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PacketReceiveHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PacketReceiveHook.cs +81 -0
@@ 0,0 1,81 @@
//
//  PacketReceiveHook.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;
using NosSmooth.LocalBinding.EventArgs;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of NetworkManager.PacketSend.
/// </summary>
internal class PacketReceiveHook : CancelableNostaleHook<IPacketReceiveHook.PacketReceiveDelegate,
    IPacketReceiveHook.PacketReceiveWrapperDelegate, PacketEventArgs>, IPacketReceiveHook
{
    /// <summary>
    /// Create the packet send hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<PacketReceiveHook> Create
        (NosBindingManager bindingManager, NosBrowserManager browserManager, HookOptions<IPacketReceiveHook> options)
    {
        var hook = CreateHook
            (
                bindingManager,
                () => new PacketReceiveHook(browserManager.NetworkManager),
                (hook) => hook.Detour,
                options
            );

        return hook;
    }

    private NetworkManager _networkManager;

    private PacketReceiveHook(NetworkManager networkManager)
    {
        _networkManager = networkManager;
    }

    /// <inheritdoc />
    public override string Name => IHookManager.PacketReceiveName;

    /// <inheritdoc />
    public override IPacketReceiveHook.PacketReceiveWrapperDelegate WrapperFunction => (packetString) =>
    {
        var packetObject = _networkManager.GetAddressForPacketReceive();
        using var nostaleString = NostaleStringA.Create(_networkManager.Memory, packetString);
        OriginalFunction(packetObject, nostaleString.Get());
    };

    /// <inheritdoc />
    protected override IPacketReceiveHook.PacketReceiveDelegate WrapWithCalling(IPacketReceiveHook.PacketReceiveDelegate function)
        => (packetObject, packetString) =>
        {
            CallingFromNosSmooth = true;
            var res = function(packetObject, packetString);
            CallingFromNosSmooth = false;
            return res;
        };

    private nuint Detour(nuint packetObject, nuint packetString)
    {
        var packet = Marshal.PtrToStringAnsi((IntPtr)packetString);
        if (packet is null)
        { // ?
            return 1;
        }

        var packetArgs = new PacketEventArgs(packet);
        return HandleCall(packetArgs);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PacketSendHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PacketSendHook.cs +82 -0
@@ 0,0 1,82 @@
//
//  PacketSendHook.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;
using NosSmooth.LocalBinding.EventArgs;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of NetworkManager.PacketSend.
/// </summary>
internal class PacketSendHook : CancelableNostaleHook<IPacketSendHook.PacketSendDelegate,
    IPacketSendHook.PacketSendWrapperDelegate, PacketEventArgs>, IPacketSendHook
{
    /// <summary>
    /// Create the packet send hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<PacketSendHook> Create
        (NosBindingManager bindingManager, NosBrowserManager browserManager, HookOptions<IPacketSendHook> options)
    {
        var hook = CreateHook
            (
                bindingManager,
                () => new PacketSendHook(browserManager.NetworkManager),
                (sendHook) => sendHook.Detour,
                options
            );

        return hook;
    }

    private PacketSendHook(NetworkManager networkManager)
    {
        _networkManager = networkManager;
    }

    private readonly NetworkManager _networkManager;

    /// <inheritdoc />
    public override string Name => IHookManager.PacketSendName;

    /// <inheritdoc />
    public override IPacketSendHook.PacketSendWrapperDelegate WrapperFunction => (packetString) =>
    {
        var packetObject = _networkManager.GetAddressForPacketSend();
        using var nostaleString = NostaleStringA.Create(_networkManager.Memory, packetString);
        OriginalFunction(packetObject, nostaleString.Get());
    };

    /// <inheritdoc />
    protected override IPacketSendHook.PacketSendDelegate WrapWithCalling(IPacketSendHook.PacketSendDelegate function)
        => (packetObject, packetString) =>
        {
            CallingFromNosSmooth = true;
            var res = function(packetObject, packetString);
            CallingFromNosSmooth = false;
            return res;
        };

    private nuint Detour(nuint packetObject, nuint packetString)
    {
        var packet = Marshal.PtrToStringAnsi((IntPtr)packetString);
        if (packet is null)
        { // ?
            return 1;
        }

        var packetArgs = new PacketEventArgs(packet);
        return HandleCall(packetArgs);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PeriodicHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PeriodicHook.cs +77 -0
@@ 0,0 1,77 @@
//
//  PeriodicHook.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.Extensions;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <inheritdoc />
internal class PeriodicHook : IPeriodicHook
{
    /// <summary>
    /// Create the periodic hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<PeriodicHook> Create(NosBindingManager bindingManager, HookOptions<IPeriodicHook> options)
    {
        var periodicHook = new PeriodicHook();

        var hookResult = bindingManager.CreateCustomAsmHookFromPattern<IPeriodicHook.PeriodicDelegate>
            (periodicHook.Name, periodicHook.Detour, options, false);
        if (!hookResult.IsDefined(out var hook))
        {
            return Result<PeriodicHook>.FromError(hookResult);
        }

        periodicHook._hook = hook;
        return periodicHook;
    }

    private PeriodicHook()
    {
    }

    private NosAsmHook<IPeriodicHook.PeriodicDelegate> _hook = null!;

    /// <inheritdoc />
    public string Name => IHookManager.PeriodicName;

    /// <inheritdoc />
    public bool IsEnabled => _hook.Hook.IsEnabled;

    /// <inheritdoc />
    public IPeriodicHook.PeriodicDelegate WrapperFunction => OriginalFunction;

    /// <inheritdoc/>
    public IPeriodicHook.PeriodicDelegate OriginalFunction => throw new InvalidOperationException
        ("Calling NosTale periodic function from NosSmooth is not allowed.");

    /// <inheritdoc />
    public event EventHandler<System.EventArgs>? Called;

    /// <inheritdoc />
    public Result Enable()
    {
        _hook.Hook.EnableOrActivate();
        return Result.FromSuccess();
    }

    /// <inheritdoc />
    public Result Disable()
    {
        _hook.Hook.Disable();
        return Result.FromSuccess();
    }

    private void Detour()
    {
        Called?.Invoke(this, System.EventArgs.Empty);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PetWalkHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PetWalkHook.cs +83 -0
@@ 0,0 1,83 @@
//
//  PetWalkHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of NetworkManager.PetWalk.
/// </summary>
internal class PetWalkHook : CancelableNostaleHook<IPetWalkHook.PetWalkDelegate,
    IPetWalkHook.PetWalkWrapperDelegate, PetWalkEventArgs>, IPetWalkHook
{
    /// <summary>
    /// Create the packet send hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<PetWalkHook> Create
        (NosBindingManager bindingManager, HookOptions<IPetWalkHook> options)
    {
        var hook = CreateHook
        (
            bindingManager,
            () => new PetWalkHook(bindingManager.Memory),
            hook => hook.Detour,
            options
        );

        return hook;
    }

    private IMemory _memory;

    private PetWalkHook(IMemory memory)
    {
        _memory = memory;
    }

    /// <inheritdoc />
    public override string Name => IHookManager.PetWalkName;

    /// <inheritdoc />
    public override IPetWalkHook.PetWalkWrapperDelegate WrapperFunction
        => (p, x, y) => OriginalFunction(p.Address, ((int)x << 16) | (int)y) == 1;

    /// <inheritdoc />
    protected override IPetWalkHook.PetWalkDelegate WrapWithCalling(IPetWalkHook.PetWalkDelegate function)
        =>
            (
                petManagerPtr,
                position,
                un0,
                un1
            ) =>
            {
                CallingFromNosSmooth = true;
                var res = function(petManagerPtr, position, un0, un1);
                CallingFromNosSmooth = false;
                return res;
            };

    private nuint Detour
    (
        nuint petManagerPtr,
        int position,
        short un0,
        int un1
    )
    {
        var petManager = new PetManager(_memory, petManagerPtr);
        var walkArgs = new PetWalkEventArgs
            (petManager, (ushort)((position >> 16) & 0xFFFF), (ushort)(position & 0xFFFF));
        return HandleCall(walkArgs);
    }
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PlayerWalkHook.cs => src/Core/NosSmooth.LocalBinding/Hooks/Implementations/PlayerWalkHook.cs +72 -0
@@ 0,0 1,72 @@
//
//  PlayerWalkHook.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.EventArgs;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.LocalBinding.Hooks.Implementations;

/// <summary>
/// A hook of NetworkManager.PlayerWalk.
/// </summary>
internal class PlayerWalkHook : CancelableNostaleHook<IPlayerWalkHook.WalkDelegate,
    IPlayerWalkHook.WalkWrapperDelegate, WalkEventArgs>, IPlayerWalkHook
{
    /// <summary>
    /// Create the packet send hook.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A packet send hook or an error.</returns>
    public static Result<PlayerWalkHook> Create
        (NosBindingManager bindingManager, NosBrowserManager browserManager, HookOptions<IPlayerWalkHook> options)
    {
        var hook = CreateHook
            (
                bindingManager,
                () => new PlayerWalkHook(browserManager.PlayerManager),
                hook => hook.Detour,
                options
            );

        return hook;
    }

    private PlayerWalkHook(PlayerManager playerManager)
    {
        _playerManager = playerManager;
    }

    private PlayerManager _playerManager;

    /// <inheritdoc />
    public override string Name => IHookManager.CharacterWalkName;

    /// <inheritdoc />
    public override IPlayerWalkHook.WalkWrapperDelegate WrapperFunction => (x, y) =>
    {
        var playerManagerObject = _playerManager.Address;
        return OriginalFunction(playerManagerObject, ((int)x << 16) | (int)y) == 1;
    };

    /// <inheritdoc />
    protected override IPlayerWalkHook.WalkDelegate WrapWithCalling(IPlayerWalkHook.WalkDelegate function)
        => (playerManagerPtr, position, un0, un1) =>
        {
            CallingFromNosSmooth = true;
            var res = function(playerManagerPtr, position, un0, un1);
            CallingFromNosSmooth = false;
            return res;
        };

    private nuint Detour(nuint playerManagerPtr, int position, short un0, int un1)
    {
        var walkArgs = new WalkEventArgs((ushort)((position >> 16) & 0xFFFF), (ushort)(position & 0xFFFF));
        return HandleCall(walkArgs);
    }
}
\ No newline at end of file

M src/Core/NosSmooth.LocalBinding/IsExternalInit.cs => src/Core/NosSmooth.LocalBinding/IsExternalInit.cs +1 -1
@@ 4,7 4,7 @@
//  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 System.Runtime.CompilerServices
namespace NosSmooth.LocalBinding
{
    /// <summary>
    /// Dummy.

M src/Core/NosSmooth.LocalBinding/NosBindingManager.cs => src/Core/NosSmooth.LocalBinding/NosBindingManager.cs +50 -342
@@ 7,6 7,8 @@
using System.Diagnostics;
using Microsoft.Extensions.Options;
using NosSmooth.LocalBinding.Errors;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Hooks.Implementations;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Options;
using Reloaded.Hooks;


@@ 26,46 28,24 @@ namespace NosSmooth.LocalBinding;
public class NosBindingManager : IDisposable
{
    private readonly NosBrowserManager _browserManager;
    private readonly PetManagerBindingOptions _petManagerBindingOptions;
    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;
    private readonly IHookManager _hookManager;

    /// <summary>
    /// Initializes a new instance of the <see cref="NosBindingManager"/> class.
    /// </summary>
    /// <param name="browserManager">The NosTale browser manager.</param>
    /// <param name="characterBindingOptions">The character binding options.</param>
    /// <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>
    /// <param name="hookManager">The hook manager.</param>
    public NosBindingManager
    (
        NosBrowserManager browserManager,
        IOptions<CharacterBindingOptions> characterBindingOptions,
        IOptions<NetworkBindingOptions> networkBindingOptions,
        IOptions<UnitManagerBindingOptions> sceneManagerBindingOptions,
        IOptions<PetManagerBindingOptions> petManagerBindingOptions,
        IOptions<PeriodicBindingOptions> periodicBindingOptions
        IHookManager hookManager
    )
    {
        _browserManager = browserManager;
        _hookManager = hookManager;
        Hooks = new ReloadedHooks();
        Memory = new Memory();
        Scanner = new Scanner(Process.GetCurrentProcess(), Process.GetCurrentProcess().MainModule);
        _characterBindingOptions = characterBindingOptions.Value;
        _networkBindingOptions = networkBindingOptions.Value;
        _unitManagerBindingOptions = sceneManagerBindingOptions.Value;
        _petManagerBindingOptions = petManagerBindingOptions.Value;
        _periodicBindingOptions = periodicBindingOptions.Value;
    }

    /// <summary>


@@ 84,116 64,11 @@ 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>
    public NetworkBinding Network
    {
        get
        {
            if (_networkBinding is null)
            {
                throw new InvalidOperationException
                (
                    "Could not get network. The binding manager is not initialized. Did you forget to call NosBindingManager.Initialize?"
                );
            }

            return _networkBinding;
        }
    }

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

            return _characterBinding;
        }
    }

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

            return _unitManagerBinding;
        }
    }

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

            return _petManagerBinding;
        }
    }

    /// <summary>
    /// Initialize the existing bindings and hook NosTale functions.
    /// </summary>
    /// <returns>A result that may or may not have succeeded.</returns>
    public IResult Initialize()
    {
        if (_networkBinding is not null)
        { // already initialized
            return Result.FromSuccess();
        }

        List<IResult> errorResults = new List<IResult>();
        var browserInitializationResult = _browserManager.Initialize();
        if (!browserInitializationResult.IsSuccess)


@@ 206,163 81,10 @@ 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);
            if (!network.IsSuccess)
            {
                errorResults.Add
                (
                    Result.FromError
                    (
                        new CouldNotInitializeModuleError(typeof(NetworkBinding), network.Error),
                        network
                    )
                );
            }

            _networkBinding = network.Entity;
        }
        catch (Exception e)
        {
            errorResults.Add
            (
                Result.FromError
                (
                    new CouldNotInitializeModuleError(typeof(NetworkBinding), new ExceptionError(e)),
                    (Result)new ExceptionError(e)
                )
            );
        }

        try
        {
            var playerManagerBinding = PlayerManagerBinding.Create
            (
                this,
                _browserManager.PlayerManager,
                _characterBindingOptions
            );
            if (!playerManagerBinding.IsSuccess)
            {
                errorResults.Add
                (
                    Result.FromError
                    (
                        new CouldNotInitializeModuleError(typeof(PlayerManagerBinding), playerManagerBinding.Error),
                        playerManagerBinding
                    )
                );
            }
            _characterBinding = playerManagerBinding.Entity;
        }
        catch (Exception e)
        var hookManagerInitializationResult = _hookManager.Initialize(this, _browserManager);
        if (!hookManagerInitializationResult.IsSuccess)
        {
            errorResults.Add
            (
                Result.FromError
                (
                    new CouldNotInitializeModuleError(typeof(PlayerManagerBinding), new ExceptionError(e)),
                    (Result)new ExceptionError(e)
                )
            );
        }

        try
        {
            var unitManagerBinding = UnitManagerBinding.Create
            (
                this,
                _unitManagerBindingOptions
            );
            if (!unitManagerBinding.IsSuccess)
            {
                errorResults.Add
                (
                    Result.FromError
                    (
                        new CouldNotInitializeModuleError(typeof(UnitManagerBinding), unitManagerBinding.Error),
                        unitManagerBinding
                    )
                );
            }
            _unitManagerBinding = unitManagerBinding.Entity;
        }
        catch (Exception e)
        {
            errorResults.Add
            (
                Result.FromError
                (
                    new CouldNotInitializeModuleError(typeof(UnitManagerBinding), new ExceptionError(e)),
                    (Result)new ExceptionError(e)
                )
            );
        }

        try
        {
            var petManagerBinding = PetManagerBinding.Create
            (
                this,
                _browserManager.PetManagerList,
                _petManagerBindingOptions
            );
            if (!petManagerBinding.IsSuccess)
            {
                errorResults.Add
                (
                    Result.FromError
                    (
                        new CouldNotInitializeModuleError(typeof(PetManagerBinding), petManagerBinding.Error),
                        petManagerBinding
                    )
                );
            }
            _petManagerBinding = petManagerBinding.Entity;
        }
        catch (Exception e)
        {
            errorResults.Add
            (
                Result.FromError
                (
                    new CouldNotInitializeModuleError(typeof(UnitManagerBinding), new ExceptionError(e)),
                    (Result)new ExceptionError(e)
                )
            );
            errorResults.Add(hookManagerInitializationResult);
        }

        return errorResults.Count switch


@@ 373,33 95,11 @@ public class NosBindingManager : IDisposable
        };
    }

    /// <summary>
    /// Disable the currently enabled NosTale hooks.
    /// </summary>
    public void DisableHooks()
    {
        _networkBinding?.DisableHooks();
        _characterBinding?.DisableHooks();
        _unitManagerBinding?.DisableHooks();
        _petManagerBinding?.DisableHooks();
    }

    /// <summary>
    /// Enable all NosTale hooks.
    /// </summary>
    public void EnableHooks()
    {
        _networkBinding?.EnableHooks();
        _characterBinding?.EnableHooks();
        _unitManagerBinding?.EnableHooks();
        _petManagerBinding?.EnableHooks();
    }

    /// <inheritdoc />
    public void Dispose()
    {
        Scanner.Dispose();
        DisableHooks();
        _hookManager.DisableAll();
    }

    /// <summary>


@@ 457,6 157,7 @@ public class NosBindingManager : IDisposable
    /// <param name="name">The name of the binding.</param>
    /// <param name="callbackFunction">The callback function to call instead of the original one.</param>
    /// <param name="options">The options for the function hook. (pattern, offset, whether to activate).</param>
    /// <param name="cancellable">Whether the call may be cancelled.</param>
    /// <typeparam name="TFunction">The type of the function.</typeparam>
    /// <returns>The hook object or an error.</returns>
    internal Result<NosAsmHook<TFunction>>


@@ 464,7 165,8 @@ public class NosBindingManager : IDisposable
        (
            string name,
            TFunction callbackFunction,
            HookOptions options
            HookOptions options,
            bool cancellable = true
        )
    {
        var walkFunctionAddress = Scanner.FindPattern(options.MemoryPattern);


@@ 482,41 184,47 @@ public class NosBindingManager : IDisposable
            var callDetour = Utilities.GetAbsoluteCallMnemonics
                (reverseWrapper.WrapperPointer.ToUnsigned(), IntPtr.Size == 8);

            var hook = Hooks.CreateAsmHook
            (
                new[]
                {
                    "use32",
                    "pushad",
                    "pushfd",
                    "push edx",
                    "push eax",
            var asmInstructions = new List<string>();

                    // call managed function
                    callDetour,
            asmInstructions.AddRange(new[]
            {
                "use32",
                "pushad",
                "pushfd",

                    // check result
                    // 1 means continue executing
                    // 0 means do not permit the call
                    "test eax, eax",
                    "jnz rest",
                // call managed function
                callDetour
            });

                    // returned 0, going to return early
                    "pop eax",
                    "pop edx",
                    "popfd",
                    "popad",
                    "ret", // return early
            if (cancellable)
            {
                asmInstructions.AddRange
                (
                    new[]
                    {
                        // check result
                        // 1 means continue executing
                        // 0 means do not permit the call
                        "test eax, eax",
                        "jnz rest",

                        // returned 0, going to return early
                        "popfd",
                        "popad",
                        "ret", // return early
                    }
                );
            }

                    // returned 1, going to execute the function
                    "rest:",
                    "pop eax",
                    "pop edx",
                    "popfd",
                    "popad"
                },
                address
            );
            asmInstructions.AddRange(new[]
            {
                // returned 1, going to execute the function
                "rest:",
                "popfd",
                "popad"
            });

            var hook = Hooks.CreateAsmHook(asmInstructions.ToArray(), address);

            if (options.Hook)
            {

M src/Core/NosSmooth.LocalBinding/NosBrowserManager.cs => src/Core/NosSmooth.LocalBinding/NosBrowserManager.cs +96 -4
@@ 56,9 56,13 @@ public class NosBrowserManager
    private readonly PlayerManagerOptions _playerManagerOptions;
    private readonly SceneManagerOptions _sceneManagerOptions;
    private readonly PetManagerOptions _petManagerOptions;
    private readonly NetworkManagerOptions _networkManagerOptions;
    private readonly UnitManagerOptions _unitManagerOptions;
    private PlayerManager? _playerManager;
    private SceneManager? _sceneManager;
    private PetManagerList? _petManagerList;
    private NetworkManager? _networkManager;
    private UnitManager? _unitManager;

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


@@ 66,18 70,24 @@ public class NosBrowserManager
    /// <param name="playerManagerOptions">The options for obtaining player manager.</param>
    /// <param name="sceneManagerOptions">The scene manager options.</param>
    /// <param name="petManagerOptions">The pet manager options.</param>
    /// <param name="networkManagerOptions">The network manager options.</param>
    /// <param name="unitManagerOptions">The unit manager options.</param>
    public NosBrowserManager
    (
        IOptions<PlayerManagerOptions> playerManagerOptions,
        IOptions<SceneManagerOptions> sceneManagerOptions,
        IOptions<PetManagerOptions> petManagerOptions
        IOptions<PetManagerOptions> petManagerOptions,
        IOptions<NetworkManagerOptions> networkManagerOptions,
        IOptions<UnitManagerOptions> unitManagerOptions
    )
        : this
        (
            Process.GetCurrentProcess(),
            playerManagerOptions.Value,
            sceneManagerOptions.Value,
            petManagerOptions.Value
            petManagerOptions.Value,
            networkManagerOptions.Value,
            unitManagerOptions.Value
        )
    {
    }


@@ 89,19 99,25 @@ public class NosBrowserManager
    /// <param name="playerManagerOptions">The options for obtaining player manager.</param>
    /// <param name="sceneManagerOptions">The scene manager options.</param>
    /// <param name="petManagerOptions">The pet manager options.</param>
    /// <param name="networkManagerOptions">The network manager options.</param>
    /// <param name="unitManagerOptions">The unit manager options.</param>
    public NosBrowserManager
    (
        Process process,
        PlayerManagerOptions playerManagerOptions,
        SceneManagerOptions sceneManagerOptions,
        PetManagerOptions petManagerOptions
        PetManagerOptions petManagerOptions,
        NetworkManagerOptions networkManagerOptions,
        UnitManagerOptions unitManagerOptions
    )
    {
        _playerManagerOptions = playerManagerOptions;
        _sceneManagerOptions = sceneManagerOptions;
        _petManagerOptions = petManagerOptions;
        _networkManagerOptions = networkManagerOptions;
        _unitManagerOptions = unitManagerOptions;
        Process = process;
        Memory = new ExternalMemory(process);
        Memory = Process.Id == Process.GetCurrentProcess().Id ? new Memory() : new ExternalMemory(process);
        Scanner = new Scanner(process, process.MainModule);
    }



@@ 126,6 142,46 @@ public class NosBrowserManager
    public bool IsNostaleProcess => NosBrowserManager.IsProcessNostaleProcess(Process);

    /// <summary>
    /// Gets the network manager.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if the browser is not initialized or there was an error with initialization of network manager.</exception>
    public NetworkManager NetworkManager
    {
        get
        {
            if (_networkManager is null)
            {
                throw new InvalidOperationException
                (
                    "Could not get network manager. The browser manager is not initialized. Did you forget to call NosBrowserManager.Initialize?"
                );
            }

            return _networkManager;
        }
    }

    /// <summary>
    /// Gets the network manager.
    /// </summary>
    /// <exception cref="InvalidOperationException">Thrown if the browser is not initialized or there was an error with initialization of unit manager.</exception>
    public UnitManager UnitManager
    {
        get
        {
            if (_unitManager is null)
            {
                throw new InvalidOperationException
                (
                    "Could not get unit manager. The browser manager is not initialized. Did you forget to call NosBrowserManager.Initialize?"
                );
            }

            return _unitManager;
        }
    }

    /// <summary>
    /// Gets whether the player is currently in game.
    /// </summary>
    /// <remarks>


@@ 215,6 271,42 @@ public class NosBrowserManager
        }

        List<IResult> errorResults = new List<IResult>();
        if (_unitManager is null)
        {
            var unitManagerResult = UnitManager.Create(this, _unitManagerOptions);
            if (!unitManagerResult.IsSuccess)
            {
                errorResults.Add
                (
                    Result.FromError
                    (
                        new CouldNotInitializeModuleError(typeof(UnitManager), unitManagerResult.Error),
                        unitManagerResult
                    )
                );
            }

            _unitManager = unitManagerResult.Entity;
        }

        if (_networkManager is null)
        {
            var networkManagerResult = NetworkManager.Create(this, _networkManagerOptions);
            if (!networkManagerResult.IsSuccess)
            {
                errorResults.Add
                (
                    Result.FromError
                    (
                        new CouldNotInitializeModuleError(typeof(NetworkManager), networkManagerResult.Error),
                        networkManagerResult
                    )
                );
            }

            _networkManager = networkManagerResult.Entity;
        }

        if (_playerManager is null)
        {
            var playerManagerResult = PlayerManager.Create(this, _playerManagerOptions);

M src/Core/NosSmooth.LocalBinding/NosSmooth.LocalBinding.csproj => src/Core/NosSmooth.LocalBinding/NosSmooth.LocalBinding.csproj +1 -0
@@ 9,6 9,7 @@

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
      <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
      <PackageReference Include="Microsoft.Extensions.Options" Version="7.0.0" />
      <PackageReference Include="Reloaded.Hooks" Version="4.2.1" />
      <PackageReference Include="Reloaded.Memory.Sigscan" Version="3.1.6" />

M src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs => src/Core/NosSmooth.LocalBinding/NosThreadSynchronizer.cs +39 -15
@@ 5,10 5,9 @@
//  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.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Options;
using Remora.Results;



@@ 19,18 18,26 @@ namespace NosSmooth.LocalBinding;
/// </summary>
public class NosThreadSynchronizer
{
    private readonly PeriodicBinding _periodicBinding;
    private readonly IPeriodicHook _periodicHook;
    private readonly ILogger<NosThreadSynchronizer> _logger;
    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="periodicHook">The periodic hook.</param>
    /// <param name="logger">The logger.</param>
    /// <param name="options">The options.</param>
    public NosThreadSynchronizer(PeriodicBinding periodicBinding, IOptions<NosThreadSynchronizerOptions> options)
    public NosThreadSynchronizer
    (
        IPeriodicHook periodicHook,
        ILogger<NosThreadSynchronizer> logger,
        IOptions<NosThreadSynchronizerOptions> options
    )
    {
        _periodicBinding = periodicBinding;
        _periodicHook = periodicHook;
        _logger = logger;
        _options = options.Value;
        _queuedOperations = new ConcurrentQueue<SyncOperation>();
    }


@@ 40,7 47,7 @@ public class NosThreadSynchronizer
    /// </summary>
    public void StartSynchronizer()
    {
        _periodicBinding.PeriodicCall += PeriodicCall;
        _periodicHook.Called += PeriodicCall;
    }

    /// <summary>


@@ 48,7 55,7 @@ public class NosThreadSynchronizer
    /// </summary>
    public void StopSynchronizer()
    {
        _periodicBinding.PeriodicCall -= PeriodicCall;
        _periodicHook.Called -= PeriodicCall;
    }

    private void PeriodicCall(object? owner, System.EventArgs eventArgs)


@@ 65,13 72,13 @@ public class NosThreadSynchronizer
    {
        try
        {
            var result = operation.action();
            var result = operation.Action();
            operation.Result = result;
        }
        catch (Exception e)
        {
            // TODO: log?
            operation.Result = e;
            _logger.LogError(e, "Synchronizer obtained an exception");
            operation.Result = (Result)e;
        }

        if (operation.CancellationTokenSource is not null)


@@ 115,6 122,23 @@ public class NosThreadSynchronizer
    /// <returns>The result of the action.</returns>
    public async Task<Result> SynchronizeAsync(Func<Result> action, CancellationToken ct = default)
    {
        return (Result)await CommonSynchronizeAsync(() => action(), ct);
    }

    /// <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>
    /// <typeparam name="T">The type of the result.</typeparam>
    public async Task<Result<T>> SynchronizeAsync<T>(Func<Result<T>> action, CancellationToken ct = default)
    {
        return (Result<T>)await CommonSynchronizeAsync(() => action(), ct);
    }

    private async Task<IResult> CommonSynchronizeAsync(Func<IResult> action, CancellationToken ct = default)
    {
        var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(ct);
        var syncOperation = new SyncOperation(action, linkedSource);
        _queuedOperations.Enqueue(syncOperation);


@@ 132,14 156,14 @@ public class NosThreadSynchronizer
        }
        catch (Exception e)
        {
            return new ExceptionError(e);
            return (Result)new ExceptionError(e);
        }

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

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

D src/Core/NosSmooth.LocalBinding/Objects/NetworkBinding.cs => src/Core/NosSmooth.LocalBinding/Objects/NetworkBinding.cs +0 -227
@@ 1,227 0,0 @@
//
//  NetworkBinding.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.Diagnostics;
using System.Runtime.InteropServices;
using NosSmooth.LocalBinding.Errors;
using NosSmooth.LocalBinding.EventArgs;
using NosSmooth.LocalBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using Reloaded.Hooks.Definitions;
using Reloaded.Hooks.Definitions.X86;
using Remora.Results;

namespace NosSmooth.LocalBinding.Objects;

/// <summary>
/// The binding to nostale network object.
/// </summary>
public class NetworkBinding
{
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    private delegate nuint PacketSendDelegate(nuint packetObject, nuint packetString);

    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    private delegate nuint PacketReceiveDelegate(nuint packetObject, nuint packetString);

    /// <summary>
    /// Create the network binding with finding the network object and functions.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A network binding or an error.</returns>
    public static Result<NetworkBinding> Create(NosBindingManager bindingManager, NetworkBindingOptions options)
    {
        var process = Process.GetCurrentProcess();
        var networkObjectAddress = bindingManager.Scanner.FindPattern(options.NetworkObjectPattern);
        if (!networkObjectAddress.Found)
        {
            return new BindingNotFoundError(options.NetworkObjectPattern, "NetworkBinding");
        }

        var binding = new NetworkBinding
        (
            bindingManager,
            (nuint)(networkObjectAddress.Offset + (int)process.MainModule!.BaseAddress + 0x01)
        );

        var sendHookResult = bindingManager.CreateCustomAsmHookFromPattern<PacketSendDelegate>
            ("NetworkBinding.SendPacket", binding.SendPacketDetour, options.PacketSendHook);
        if (!sendHookResult.IsDefined(out var sendHook))
        {
            return Result<NetworkBinding>.FromError(sendHookResult);
        }

        var receiveHookResult = bindingManager.CreateCustomAsmHookFromPattern<PacketReceiveDelegate>
            ("NetworkBinding.ReceivePacket", binding.ReceivePacketDetour, options.PacketReceiveHook);
        if (!receiveHookResult.IsDefined(out var receiveHook))
        {
            return Result<NetworkBinding>.FromError(receiveHookResult);
        }

        binding._sendHook = sendHook;
        binding._receiveHook = receiveHook;
        return binding;
    }

    private readonly NosBindingManager _bindingManager;
    private readonly nuint _networkManagerAddress;
    private NosAsmHook<PacketSendDelegate> _sendHook = null!;
    private NosAsmHook<PacketReceiveDelegate> _receiveHook = null!;
    private bool _callingReceive;
    private bool _callingSend;

    private NetworkBinding
    (
        NosBindingManager bindingManager,
        nuint networkManagerAddress
    )
    {
        _bindingManager = bindingManager;
        _networkManagerAddress = networkManagerAddress;
    }

    /// <summary>
    /// Event that is called when packet send was called by NosTale.
    /// </summary>
    /// <remarks>
    /// The send must be hooked for this event to be called.
    /// </remarks>
    public event EventHandler<PacketEventArgs>? PacketSendCall;

    /// <summary>
    /// Event that is called when packet receive was called by NosTale.
    /// </summary>
    /// <remarks>
    /// The receive must be hooked for this event to be called.
    /// </remarks>
    public event EventHandler<PacketEventArgs>? PacketReceiveCall;

    /// <summary>
    /// Send the given packet.
    /// </summary>
    /// <param name="packet">The packet to send.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result SendPacket(string packet)
    {
        try
        {
            _callingSend = true;
            using var nostaleString = NostaleStringA.Create(_bindingManager.Memory, packet);
            _sendHook.OriginalFunction.GetWrapper()(GetManagerAddress(false), nostaleString.Get());
            _callingSend = false;
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

    /// <summary>
    /// Receive the given packet.
    /// </summary>
    /// <param name="packet">The packet to receive.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result ReceivePacket(string packet)
    {
        try
        {
            _callingReceive = true;
            using var nostaleString = NostaleStringA.Create(_bindingManager.Memory, packet);
            _receiveHook.OriginalFunction.GetWrapper()(GetManagerAddress(true), nostaleString.Get());
            _callingReceive = false;
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

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

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

    private nuint GetManagerAddress(bool third)
    {
        nuint networkManager = _networkManagerAddress;
        _bindingManager.Memory.Read(networkManager, out networkManager);
        _bindingManager.Memory.Read(networkManager, out networkManager);
        _bindingManager.Memory.Read(networkManager, out networkManager);

        if (third)
        {
            _bindingManager.Memory.Read(networkManager + 0x34, out networkManager);
        }

        return networkManager;
    }

    private nuint SendPacketDetour(nuint packetObject, nuint packetString)
    {
        if (_callingSend)
        {
            return 1;
        }

        var packet = Marshal.PtrToStringAnsi((IntPtr)packetString);
        if (packet is null)
        { // ?
            return 1;
        }
        var packetArgs = new PacketEventArgs(packet);
        PacketSendCall?.Invoke(this, packetArgs);
        return packetArgs.Cancel ? 0 : (nuint)1;
    }

    private nuint ReceivePacketDetour(nuint packetObject, nuint packetString)
    {
        if (_callingReceive)
        {
            return 1;
        }

        var packet = Marshal.PtrToStringAnsi((IntPtr)packetString);
        if (packet is null)
        { // ?
            return 1;
        }

        var packetArgs = new PacketEventArgs(packet);
        PacketReceiveCall?.Invoke(this, packetArgs);
        return packetArgs.Cancel ? 0 : (nuint)1;
    }
}
\ No newline at end of file

D src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs => src/Core/NosSmooth.LocalBinding/Objects/PeriodicBinding.cs +0 -82
@@ 1,82 0,0 @@
//
//  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 EventHandler? PeriodicCall;

    /// <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()
    {
        PeriodicCall?.Invoke(this, System.EventArgs.Empty);
    }
}
\ No newline at end of file

D src/Core/NosSmooth.LocalBinding/Objects/PetManagerBinding.cs => src/Core/NosSmooth.LocalBinding/Objects/PetManagerBinding.cs +0 -168
@@ 1,168 0,0 @@
//
//  PetManagerBinding.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.EventArgs;
using NosSmooth.LocalBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Hooks.Definitions;
using Reloaded.Hooks.Definitions.X86;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.LocalBinding.Objects;

/// <summary>
/// The binding to NosTale pet manager.
/// </summary>
public class PetManagerBinding
{
    private readonly IMemory _memory;

    /// <summary>
    /// Create nostale pet manager binding.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="petManagerList">The list of the pet managers.</param>
    /// <param name="options">The options.</param>
    /// <returns>A pet manager binding or and error.</returns>
    public static Result<PetManagerBinding> Create
        (NosBindingManager bindingManager, PetManagerList petManagerList, PetManagerBindingOptions options)
    {
        var petManager = new PetManagerBinding(bindingManager.Memory, petManagerList);
        var hookResult = bindingManager.CreateHookFromPattern<PetWalkDelegate>
        (
            "PetManagerBinding.PetWalk",
            petManager.PetWalkDetour,
            options.PetWalkHook
        );

        if (!hookResult.IsSuccess)
        {
            return Result<PetManagerBinding>.FromError(hookResult);
        }

        petManager._petWalkHook = hookResult.Entity;
        return petManager;
    }

    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    private delegate bool PetWalkDelegate
    (
        nuint petManagerPtr,
        uint position,
        short unknown0 = 0,
        int unknown1 = 1,
        int unknown2 = 1
    );

    private IHook<PetWalkDelegate> _petWalkHook = null!;

    private PetManagerBinding(IMemory memory, PetManagerList petManagerList)
    {
        _memory = memory;
        PetManagerList = petManagerList;
    }

    /// <summary>
    /// Event that is called when walk was called by NosTale.
    /// </summary>
    /// <remarks>
    /// The walk must be hooked for this event to be called.
    /// </remarks>
    public event EventHandler<PetWalkEventArgs>? PetWalkCall;

    /// <summary>
    /// Gets the hook of the pet walk function.
    /// </summary>
    public IHook PetWalkHook => _petWalkHook;

    /// <summary>
    /// Gets pet manager list.
    /// </summary>
    public PetManagerList PetManagerList { get; }

    /// <summary>
    /// Disable all PetManager hooks.
    /// </summary>
    public void DisableHooks()
    {
        _petWalkHook.Disable();
    }

    /// <summary>
    /// Enable all PetManager hooks.
    /// </summary>
    public void EnableHooks()
    {
        _petWalkHook.EnableOrActivate();
    }

    /// <summary>
    /// Walk the given pet to the given location.
    /// </summary>
    /// <param name="selector">Index of the pet to walk. -1 for every pet currently available.</param>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    /// <returns>A result returned from NosTale or an error.</returns>
    public Result<bool> PetWalk(int selector, short x, short y)
    {
        uint position = Convert.ToUInt32(((ushort)y << 16) | (ushort)x);
        if (PetManagerList.Length < selector + 1)
        {
            return new NotFoundError("Could not find the pet using the given selector.");
        }

        if (selector == -1)
        {
            bool lastResult = true;
            for (int i = 0; i < PetManagerList.Length; i++)
            {
                lastResult = _petWalkHook.OriginalFunction(PetManagerList[i].Address, position);
            }

            return lastResult;
        }

        return _petWalkHook.OriginalFunction(PetManagerList[selector].Address, position);
    }

    private bool PetWalkDetour
    (
        nuint petManagerPtr,
        uint position,
        short unknown0 = 0,
        int unknown1 = 1,
        int unknown2 = 1
    )
    {
        var x = (ushort)(position & 0xFFFF);
        var y = (ushort)((position >> 16) & 0xFFFF);
        var petManager = new PetManager(_memory, petManagerPtr);
        var petWalkEventArgs = new PetWalkEventArgs(petManager, x, y);
        PetWalkCall?.Invoke(this, petWalkEventArgs);

        if (!petWalkEventArgs.Cancel)
        {
            return _petWalkHook.OriginalFunction
            (
                petManagerPtr,
                position,
                unknown0,
                unknown1,
                unknown2
            );
        }

        return false;
    }
}
\ No newline at end of file

D src/Core/NosSmooth.LocalBinding/Objects/PlayerManagerBinding.cs => src/Core/NosSmooth.LocalBinding/Objects/PlayerManagerBinding.cs +0 -260
@@ 1,260 0,0 @@
//
//  PlayerManagerBinding.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 System.Diagnostics;
using NosSmooth.LocalBinding.Errors;
using NosSmooth.LocalBinding.EventArgs;
using NosSmooth.LocalBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Hooks.Definitions;
using Reloaded.Hooks.Definitions.X86;
using Remora.Results;

namespace NosSmooth.LocalBinding.Objects;

/// <summary>
/// The nostale binding of a character.
/// </summary>
public class PlayerManagerBinding
{
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    private delegate bool WalkDelegate(nuint playerManagerPtr, int position, short unknown0 = 0, int unknown1 = 1);

    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx, FunctionAttribute.Register.ecx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    private delegate bool FollowEntityDelegate
    (
        nuint playerManagerPtr,
        nuint entityPtr,
        int unknown1 = 0,
        int unknown2 = 1
    );

    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    private delegate void UnfollowEntityDelegate(nuint playerManagerPtr, int unknown = 0);

    /// <summary>
    /// Create the network binding with finding the network object and functions.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="playerManager">The player manager.</param>
    /// <param name="options">The options for the binding.</param>
    /// <returns>A network binding or an error.</returns>
    public static Result<PlayerManagerBinding> Create(NosBindingManager bindingManager, PlayerManager playerManager, CharacterBindingOptions options)
    {
        var binding = new PlayerManagerBinding
        (
            bindingManager,
            playerManager
        );

        var walkHookResult = bindingManager.CreateHookFromPattern<WalkDelegate>
            ("CharacterBinding.Walk", binding.WalkDetour, options.WalkHook);
        if (!walkHookResult.IsDefined(out var walkHook))
        {
            return Result<PlayerManagerBinding>.FromError(walkHookResult);
        }

        var entityFollowHookResult = bindingManager.CreateHookFromPattern<FollowEntityDelegate>
            ("CharacterBinding.EntityFollow", binding.FollowEntityDetour, options.EntityFollowHook);
        if (!entityFollowHookResult.IsDefined(out var entityFollowHook))
        {
            return Result<PlayerManagerBinding>.FromError(entityFollowHookResult);
        }

        var entityUnfollowHookResult = bindingManager.CreateHookFromPattern<UnfollowEntityDelegate>
            ("CharacterBinding.EntityUnfollow", binding.UnfollowEntityDetour, options.EntityUnfollowHook);
        if (!entityUnfollowHookResult.IsDefined(out var entityUnfollowHook))
        {
            return Result<PlayerManagerBinding>.FromError(entityUnfollowHookResult);
        }

        binding._walkHook = walkHook;
        binding._followHook = entityFollowHook;
        binding._unfollowHook = entityUnfollowHook;
        return binding;
    }

    private readonly NosBindingManager _bindingManager;

    private IHook<WalkDelegate> _walkHook = null!;
    private IHook<FollowEntityDelegate> _followHook = null!;
    private IHook<UnfollowEntityDelegate> _unfollowHook = null!;

    private PlayerManagerBinding
    (
        NosBindingManager bindingManager,
        PlayerManager playerManager
    )
    {
        PlayerManager = playerManager;
        _bindingManager = bindingManager;
    }

    /// <summary>
    /// Gets the player manager.
    /// </summary>
    public PlayerManager PlayerManager { get; }

    /// <summary>
    /// Event that is called when walk was called by NosTale.
    /// </summary>
    /// <remarks>
    /// The walk must be hooked for this event to be called.
    /// </remarks>
    public event EventHandler<WalkEventArgs>? WalkCall;

    /// <summary>
    /// Event that is called when entity follow or unfollow was called.
    /// </summary>
    /// <remarks>
    /// The follow/unfollow entity must be hooked for this event to be called.
    /// </remarks>
    public event EventHandler<EntityEventArgs>? FollowEntityCall;

    /// <summary>
    /// Disable all PlayerManager hooks.
    /// </summary>
    public void DisableHooks()
    {
        _followHook.Disable();
        _unfollowHook.Disable();
        _walkHook.Disable();
    }

    /// <summary>
    /// Enable all PlayerManager hooks.
    /// </summary>
    public void EnableHooks()
    {
        _followHook.EnableOrActivate();
        _unfollowHook.EnableOrActivate();
        _walkHook.EnableOrActivate();
    }

    /// <summary>
    /// Walk to the given position.
    /// </summary>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result<bool> Walk(short x, short y)
    {
        int param = ((ushort)y << 16) | (ushort)x;
        try
        {
            return _walkHook.OriginalFunction(PlayerManager.Address, param);
        }
        catch (Exception e)
        {
            return e;
        }
    }

    private bool WalkDetour(nuint characterObject, int position, short unknown0, int unknown1)
    {
        var walkEventArgs = new WalkEventArgs((ushort)(position & 0xFFFF), (ushort)((position >> 16) & 0xFFFF));
        WalkCall?.Invoke(this, walkEventArgs);
        if (!walkEventArgs.Cancel)
        {
            return _walkHook.OriginalFunction(characterObject, position, unknown0, unknown1);
        }

        return false;
    }

    /// <summary>
    /// Follow the entity.
    /// </summary>
    /// <param name="entity">The entity.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result FollowEntity(MapBaseObj? entity)
        => FollowEntity(entity?.Address ?? nuint.Zero);

    /// <summary>
    /// Follow the entity.
    /// </summary>
    /// <param name="entityAddress">The entity address.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result FollowEntity(nuint entityAddress)
    {
        try
        {
            _followHook.OriginalFunction(PlayerManager.Address, entityAddress);
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

    /// <summary>
    /// Stop following entity.
    /// </summary>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result UnfollowEntity()
    {
        try
        {
            _unfollowHook.OriginalFunction(PlayerManager.Address);
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

    private bool FollowEntityDetour
    (
        nuint playerManagerPtr,
        nuint entityPtr,
        int unknown1,
        int unknown2
    )
    {
        var entityEventArgs = new EntityEventArgs(new MapBaseObj(_bindingManager.Memory, entityPtr));
        FollowEntityCall?.Invoke(this, entityEventArgs);
        if (!entityEventArgs.Cancel)
        {
            return _followHook.OriginalFunction(playerManagerPtr, entityPtr, unknown1, unknown2);
        }

        return false;
    }

    private void UnfollowEntityDetour(nuint playerManagerPtr, int unknown)
    {
        var entityEventArgs = new EntityEventArgs(null);
        FollowEntityCall?.Invoke(this, entityEventArgs);
        if (!entityEventArgs.Cancel)
        {
            _unfollowHook.OriginalFunction(playerManagerPtr, unknown);
        }
    }
}
\ No newline at end of file

D src/Core/NosSmooth.LocalBinding/Objects/UnitManagerBinding.cs => src/Core/NosSmooth.LocalBinding/Objects/UnitManagerBinding.cs +0 -172
@@ 1,172 0,0 @@
//
//  UnitManagerBinding.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.EventArgs;
using NosSmooth.LocalBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using NosSmooth.LocalBinding.Structs;
using Reloaded.Hooks.Definitions;
using Reloaded.Hooks.Definitions.X86;
using Reloaded.Hooks.Internal.Testing;
using Remora.Results;

namespace NosSmooth.LocalBinding.Objects;

/// <summary>
/// The nostale binding of a scene manager.
/// </summary>
/// <remarks>
/// The scene manager holds addresses to entities, mouse position, ....
/// </remarks>
public class UnitManagerBinding
{
    [Function
    (
        new[] { FunctionAttribute.Register.eax, FunctionAttribute.Register.edx },
        FunctionAttribute.Register.eax,
        FunctionAttribute.StackCleanup.Callee,
        new[] { FunctionAttribute.Register.ebx, FunctionAttribute.Register.esi, FunctionAttribute.Register.edi, FunctionAttribute.Register.ebp }
    )]
    private delegate nuint FocusEntityDelegate(nuint unitManagerPtr, nuint entityPtr);

    /// <summary>
    /// Create the scene manager binding.
    /// </summary>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="bindingOptions">The options for the binding.</param>
    /// <returns>A network binding or an error.</returns>
    public static Result<UnitManagerBinding> Create
        (NosBindingManager bindingManager, UnitManagerBindingOptions bindingOptions)
    {
        var process = Process.GetCurrentProcess();

        var unitManagerStaticAddress = bindingManager.Scanner.FindPattern(bindingOptions.UnitManagerPattern);
        if (!unitManagerStaticAddress.Found)
        {
            return new BindingNotFoundError(bindingOptions.UnitManagerPattern, "UnitManagerBinding.UnitManager");
        }

        var binding = new UnitManagerBinding
        (
            bindingManager,
            (int)process.MainModule!.BaseAddress + unitManagerStaticAddress.Offset,
            bindingOptions.UnitManagerOffsets
        );

        var entityFocusHookResult = bindingManager.CreateCustomAsmHookFromPattern<FocusEntityDelegate>
            ("UnitManager.EntityFocus", binding.FocusEntityDetour, bindingOptions.EntityFocusHook);
        if (!entityFocusHookResult.IsDefined(out var entityFocusHook))
        {
            return Result<UnitManagerBinding>.FromError(entityFocusHookResult);
        }

        binding._focusHook = entityFocusHook;
        return binding;
    }

    private readonly int _staticUnitManagerAddress;
    private readonly int[] _unitManagerOffsets;

    private readonly NosBindingManager _bindingManager;

    private NosAsmHook<FocusEntityDelegate> _focusHook = null!;

    private bool _callingFocus;

    private UnitManagerBinding
    (
        NosBindingManager bindingManager,
        int staticUnitManagerAddress,
        int[] unitManagerOffsets
    )
    {
        _bindingManager = bindingManager;
        _staticUnitManagerAddress = staticUnitManagerAddress;
        _unitManagerOffsets = unitManagerOffsets;
    }

    /// <summary>
    /// Gets the address of unit manager.
    /// </summary>
    public nuint Address => _bindingManager.Memory.FollowStaticAddressOffsets
        (_staticUnitManagerAddress, _unitManagerOffsets);

    /// <summary>
    /// Event that is called when entity focus was called by NosTale.
    /// </summary>
    /// <remarks>
    /// The focus entity must be hooked for this event to be called.
    /// </remarks>
    public event EventHandler<EntityEventArgs>? EntityFocusCall;

    /// <summary>
    /// Focus the entity.
    /// </summary>
    /// <param name="entity">The entity.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result FocusEntity(MapBaseObj? entity)
        => FocusEntity(entity?.Address ?? nuint.Zero);

    /// <summary>
    /// Disable all UnitManager hooks.
    /// </summary>
    public void DisableHooks()
    {
        _focusHook.Hook.Disable();
    }

    /// <summary>
    /// Enable all UnitManager hooks.
    /// </summary>
    public void EnableHooks()
    {
        _focusHook.Hook.EnableOrActivate();
    }

    /// <summary>
    /// Focus the entity.
    /// </summary>
    /// <param name="entityAddress">The entity address.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result FocusEntity(nuint entityAddress)
    {
        try
        {
            _callingFocus = true;
            _focusHook.OriginalFunction.GetWrapper()(Address, entityAddress);
            _callingFocus = false;
        }
        catch (Exception e)
        {
            return e;
        }

        return Result.FromSuccess();
    }

    private nuint FocusEntityDetour(nuint unitManagerPtr, nuint entityId)
    {
        if (_callingFocus)
        {
            // in case this is being called from UnitManagerBinding.FocusEntity,
            // do not handle.
            return 1;
        }

        MapBaseObj? obj = null;
        if (entityId != nuint.Zero)
        {
            obj = new MapBaseObj(_bindingManager.Memory, entityId);
        }

        var entityEventArgs = new EntityEventArgs(obj);
        EntityFocusCall?.Invoke(this, entityEventArgs);

        return entityEventArgs.Cancel ? 0 : (nuint)1;
    }
}
\ No newline at end of file

D src/Core/NosSmooth.LocalBinding/Options/CharacterBindingOptions.cs => src/Core/NosSmooth.LocalBinding/Options/CharacterBindingOptions.cs +0 -32
@@ 1,32 0,0 @@
//
//  CharacterBindingOptions.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="PlayerManagerBinding"/>.
/// </summary>
public class CharacterBindingOptions
{
    /// <summary>
    /// Gets or sets the configuration for player walk function hook.
    /// </summary>
    public HookOptions WalkHook { get; set; } = new HookOptions(false, "55 8B EC 83 C4 EC 53 56 57 66 89 4D FA", 0);

    /// <summary>
    /// Gets or sets the configuration for entity follow function hook.
    /// </summary>
    public HookOptions EntityFollowHook { get; set; }
        = new HookOptions(false, "55 8B EC 51 53 56 57 88 4D FF 8B F2 8B F8", 0);

    /// <summary>
    /// Gets or sets the configuration for entity unfollow function hook.
    /// </summary>
    public HookOptions EntityUnfollowHook { get; set; }
        = new HookOptions(false, "80 78 14 00 74 1A", 0);
}
\ No newline at end of file

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

namespace NosSmooth.LocalBinding.Options;

/// <summary>
/// Options for <see cref="IHookManager"/>.
/// </summary>
public class HookManagerOptions
{
    /// <summary>
    /// Gets or sets the configuration for player walk function hook.
    /// </summary>
    public HookOptions<IPlayerWalkHook> PlayerWalkHook { get; set; }
        = new(IHookManager.CharacterWalkName, false, "55 8B EC 83 C4 EC 53 56 57 66 89 4D FA", 0);

    /// <summary>
    /// Gets or sets the configuration for entity follow function hook.
    /// </summary>
    public HookOptions<IEntityFollowHook> EntityFollowHook { get; set; }
        = new(IHookManager.EntityFollowName, false, "55 8B EC 51 53 56 57 88 4D FF 8B F2 8B F8", 0);

    /// <summary>
    /// Gets or sets the configuration for entity unfollow function hook.
    /// </summary>
    public HookOptions<IEntityUnfollowHook> EntityUnfollowHook { get; set; }
        = new(IHookManager.EntityUnfollowName, false, "80 78 14 00 74 1A", 0);

    /// <summary>
    /// Gets or sets the configuration for packet receive function hook.
    /// </summary>
    public HookOptions<IPacketReceiveHook> PacketReceiveHook { get; set; }
        = new
        (
            IHookManager.PacketReceiveName,
            true,
            "55 8B EC 83 C4 ?? 53 56 57 33 C9 89 4D ?? 89 4D ?? 89 55 ?? 8B D8 8B 45 ??",
            0
        );

    /// <summary>
    /// Gets or sets the configuration for packet send function hook.
    /// </summary>
    public HookOptions<IPacketSendHook> PacketSendHook { get; set; }
        = new(IHookManager.PacketSendName, true, "53 56 8B F2 8B D8 EB 04", 0);

    /// <summary>
    /// Gets or sets the configuration for pet walk function hook.
    /// </summary>
    public HookOptions<IPetWalkHook> PetWalkHook { get; set; }
        = new(IHookManager.PetWalkName, false, "55 8b ec 83 c4 e4 53 56 57 8b f9 89 55 fc 8b d8 c6 45 fb 00", 0);

    /// <summary>
    /// Gets or sets the configuration for any periodic function hook.
    /// </summary>
    public HookOptions<IPeriodicHook> PeriodicHook { get; set; }
        = new(IHookManager.PeriodicName, true, "55 8B EC 53 56 83 C4", 0);

    /// <summary>
    /// Gets or sets the configuration for entity focus function hook.
    /// </summary>
    public HookOptions<IEntityFocusHook> EntityFocusHook { get; set; }
        = new(IHookManager.EntityFocusName, false, "73 00 00 00 55 8b ec b9 05 00 00 00", 4);
}
\ No newline at end of file

R src/Core/NosSmooth.LocalBinding/Options/NetworkBindingOptions.cs => src/Core/NosSmooth.LocalBinding/Options/NetworkManagerOptions.cs +10 -15
@@ 1,31 1,21 @@
//
//  NetworkBindingOptions.cs
//  NetworkManagerOptions.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.Hooks;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;

namespace NosSmooth.LocalBinding.Options;

/// <summary>
/// Options for <see cref="NetworkBinding"/>.
/// Options for <see cref="NetworkManager"/>.
/// </summary>
public class NetworkBindingOptions
public class NetworkManagerOptions
{
    /// <summary>
    /// Gets or sets the configuration for packet receive function hook.
    /// </summary>
    public HookOptions PacketReceiveHook { get; set; }
        = new HookOptions(true, "55 8B EC 83 C4 ?? 53 56 57 33 C9 89 4D ?? 89 4D ?? 89 55 ?? 8B D8 8B 45 ??", 0);

    /// <summary>
    /// Gets or sets the configuration for packet send function hook.
    /// </summary>
    public HookOptions PacketSendHook { get; set; }
        = new HookOptions(true, "53 56 8B F2 8B D8 EB 04", 0);

    /// <summary>
    /// Gets or sets the pattern to find the network object at.
    /// </summary>
    /// <remarks>


@@ 33,4 23,9 @@ public class NetworkBindingOptions
    /// </remarks>
    public string NetworkObjectPattern { get; set; }
        = "A1 ?? ?? ?? ?? 8B 00 BA ?? ?? ?? ?? E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? A1 ?? ?? ?? ?? 8B 00 8B 40 40";

    /// <summary>
    /// Gets or sets the offset of NetworkObject.
    /// </summary>
    public int NetworkObjectOffset { get; set; } = 1;
}
\ No newline at end of file

D src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs => src/Core/NosSmooth.LocalBinding/Options/PeriodicBindingOptions.cs +0 -21
@@ 1,21 0,0 @@
//
//  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

D src/Core/NosSmooth.LocalBinding/Options/PetManagerBindingOptions.cs => src/Core/NosSmooth.LocalBinding/Options/PetManagerBindingOptions.cs +0 -21
@@ 1,21 0,0 @@
//
//  PetManagerBindingOptions.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="PetManagerBinding"/>.
/// </summary>
public class PetManagerBindingOptions
{
    /// <summary>
    /// Gets or sets the configuration for pet walk function hook.
    /// </summary>
    public HookOptions PetWalkHook { get; set; }
        = new HookOptions(false, "55 8b ec 83 c4 e4 53 56 57 8b f9 89 55 fc 8b d8 c6 45 fb 00", 0);
}
\ No newline at end of file

R src/Core/NosSmooth.LocalBinding/Options/UnitManagerBindingOptions.cs => src/Core/NosSmooth.LocalBinding/Options/UnitManagerOptions.cs +5 -9
@@ 1,17 1,19 @@
//
//  UnitManagerBindingOptions.cs
//  UnitManagerOptions.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.Hooks;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;

namespace NosSmooth.LocalBinding.Options;

/// <summary>
/// Options for <see cref="UnitManagerBinding"/>.
/// Options for <see cref="UnitManager"/>.
/// </summary>
public class UnitManagerBindingOptions
public class UnitManagerOptions
{
    /// <summary>
    /// Gets or sets the pattern to static address of unit manager.


@@ 24,10 26,4 @@ public class UnitManagerBindingOptions
    /// </summary>
    public int[] UnitManagerOffsets { get; set; }
        = { 1, 0 };

    /// <summary>
    /// Gets or sets the configuration for entity focus function hook.
    /// </summary>
    public HookOptions EntityFocusHook { get; set; }
        = new HookOptions(false, "73 00 00 00 55 8b ec b9 05 00 00 00", 4);
}
\ No newline at end of file

A src/Core/NosSmooth.LocalBinding/Structs/MapBaseList.cs => src/Core/NosSmooth.LocalBinding/Structs/MapBaseList.cs +29 -0
@@ 0,0 1,29 @@
//
//  MapBaseList.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 Reloaded.Memory.Sources;

namespace NosSmooth.LocalBinding.Structs;

/// <inheritdoc />
public class MapBaseList : NostaleList<MapBaseObj>
{
    /// <summary>
    /// Initializes a new instance of the <see cref="MapBaseList"/> class.
    /// </summary>
    /// <param name="memory">The memory.</param>
    /// <param name="objListPointer">The list pointer.</param>
    public MapBaseList(IMemory memory, nuint objListPointer)
        : base(memory, objListPointer)
    {
    }

    /// <inheritdoc />
    protected override MapBaseObj CreateNew(IMemory memory, nuint address)
    {
        return new MapBaseObj(memory, address);
    }
}
\ No newline at end of file

M src/Core/NosSmooth.LocalBinding/Structs/MapBaseObj.cs => src/Core/NosSmooth.LocalBinding/Structs/MapBaseObj.cs +0 -7
@@ 16,13 16,6 @@ public class MapBaseObj : NostaleObject
    /// <summary>
    /// Initializes a new instance of the <see cref="MapBaseObj"/> class.
    /// </summary>
    public MapBaseObj()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="MapBaseObj"/> class.
    /// </summary>
    /// <param name="memory">The memory.</param>
    /// <param name="mapObjPointer">The map object pointer.</param>
    public MapBaseObj(IMemory memory, nuint mapObjPointer)

A src/Core/NosSmooth.LocalBinding/Structs/NetworkManager.cs => src/Core/NosSmooth.LocalBinding/Structs/NetworkManager.cs +92 -0
@@ 0,0 1,92 @@
//
//  NetworkManager.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.Errors;
using NosSmooth.LocalBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.LocalBinding.Structs;

/// <summary>
/// An object used for PacketSend and PacketReceive functions.
/// </summary>
public class NetworkManager : NostaleObject
{
    /// <summary>
    /// Create <see cref="PlayerManager"/> instance.
    /// </summary>
    /// <param name="nosBrowserManager">The NosTale process browser.</param>
    /// <param name="options">The options.</param>
    /// <returns>The player manager or an error.</returns>
    public static Result<NetworkManager> Create(NosBrowserManager nosBrowserManager, NetworkManagerOptions options)
    {
        var networkObjectAddress = nosBrowserManager.Scanner.FindPattern(options.NetworkObjectPattern);
        if (!networkObjectAddress.Found)
        {
            return new BindingNotFoundError(options.NetworkObjectPattern, "NetworkBinding");
        }

        if (nosBrowserManager.Process.MainModule is null)
        {
            return new NotFoundError("Cannot find the main module of the target process.");
        }

        var staticAddress = (nuint)(nosBrowserManager.Process.MainModule.BaseAddress + networkObjectAddress.Offset
            + options.NetworkObjectOffset);
        return new NetworkManager(nosBrowserManager.Memory, staticAddress);
    }

    private readonly IMemory _memory;
    private readonly nuint _staticNetworkManagerAddress;

    /// <summary>
    /// Initializes a new instance of the <see cref="NetworkManager"/> class.
    /// </summary>
    /// <param name="memory">The memory.</param>
    /// <param name="staticNetworkManagerAddress">The pointer to the beginning of the player manager structure.</param>
    public NetworkManager(IMemory memory, nuint staticNetworkManagerAddress)
        : base(memory, staticNetworkManagerAddress)
    {
        _memory = memory;
        _staticNetworkManagerAddress = staticNetworkManagerAddress;
    }

    /// <summary>
    /// Gets the address to the player manager.
    /// </summary>
    public override nuint Address => GetManagerAddress(false);

    private nuint GetManagerAddress(bool third)
    {
        nuint networkManager = _staticNetworkManagerAddress;
        _memory.Read(networkManager, out networkManager);
        _memory.Read(networkManager, out networkManager);
        _memory.Read(networkManager, out networkManager);

        if (third)
        {
            _memory.Read(networkManager + 0x34, out networkManager);
        }

        return networkManager;
    }

    /// <summary>
    /// Gets an address pointer used in PacketSend function.
    /// </summary>
    /// <returns>Pointer to the object.</returns>
    public nuint GetAddressForPacketSend()
        => GetManagerAddress(false);

    /// <summary>
    /// Gets an address pointer used in PacketReceive function.
    /// </summary>
    /// <returns>Pointer to the object.</returns>
    public nuint GetAddressForPacketReceive()
        => GetManagerAddress(true);
}
\ No newline at end of file

M src/Core/NosSmooth.LocalBinding/Structs/NostaleList.cs => src/Core/NosSmooth.LocalBinding/Structs/NostaleList.cs +13 -7
@@ 5,6 5,8 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices.ObjectiveC;
using Reloaded.Memory.Pointers;
using Reloaded.Memory.Sources;



@@ 14,8 16,8 @@ namespace NosSmooth.LocalBinding.Structs;
/// A class representing a list from nostale.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
public class NostaleList<T> : IEnumerable<T>
    where T : NostaleObject, new()
public abstract class NostaleList<T> : IEnumerable<T>
    where T : NostaleObject
{
    private readonly IMemory _memory;



@@ 52,11 54,7 @@ public class NostaleList<T> : IEnumerable<T>
            _memory.SafeRead(Address + 0x04, out int arrayAddress);
            _memory.SafeRead((nuint)arrayAddress + (nuint)(0x04 * index), out int objectAddress);

            return new T
            {
                Memory = _memory,
                Address = (nuint)objectAddress
            };
            return CreateNew(_memory, (nuint)objectAddress);
        }
    }



@@ 72,6 70,14 @@ public class NostaleList<T> : IEnumerable<T>
        }
    }

    /// <summary>
    /// Create a new instance of <see cref="T"/> with the given memory and address.
    /// </summary>
    /// <param name="memory">The memory.</param>
    /// <param name="address">The address.</param>
    /// <returns>The new object.</returns>
    protected abstract T CreateNew(IMemory memory, nuint address);

    /// <inheritdoc/>
    public IEnumerator<T> GetEnumerator()
    {

M src/Core/NosSmooth.LocalBinding/Structs/NostaleObject.cs => src/Core/NosSmooth.LocalBinding/Structs/NostaleObject.cs +0 -7
@@ 16,13 16,6 @@ public abstract class NostaleObject
    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleObject"/> class.
    /// </summary>
    internal NostaleObject()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="NostaleObject"/> class.
    /// </summary>
    /// <param name="memory">The memory.</param>
    /// <param name="address">The address in the memory.</param>
    protected NostaleObject(IMemory memory, nuint address)

M src/Core/NosSmooth.LocalBinding/Structs/PetManagerList.cs => src/Core/NosSmooth.LocalBinding/Structs/PetManagerList.cs +6 -0
@@ 50,4 50,10 @@ public class PetManagerList : NostaleList<PetManager>
        : base(memory, memory.FollowStaticAddressOffsets(staticPetManagerListAddress, staticPetManagerOffsets))
    {
    }

    /// <inheritdoc />
    protected override PetManager CreateNew(IMemory memory, nuint address)
    {
        return new PetManager(memory, address);
    }
}
\ No newline at end of file

M src/Core/NosSmooth.LocalBinding/Structs/SceneManager.cs => src/Core/NosSmooth.LocalBinding/Structs/SceneManager.cs +4 -4
@@ 65,22 65,22 @@ public class SceneManager
    /// <summary>
    /// Gets the player list.
    /// </summary>
    public NostaleList<MapBaseObj> PlayerList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0xC));
    public NostaleList<MapBaseObj> PlayerList => new MapBaseList(_memory, ReadPtr(Address + 0xC));

    /// <summary>
    /// Gets the monster list.
    /// </summary>
    public NostaleList<MapBaseObj> MonsterList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0x10));
    public NostaleList<MapBaseObj> MonsterList => new MapBaseList(_memory, ReadPtr(Address + 0x10));

    /// <summary>
    /// Gets the npc list.
    /// </summary>
    public NostaleList<MapBaseObj> NpcList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0x14));
    public NostaleList<MapBaseObj> NpcList => new MapBaseList(_memory, ReadPtr(Address + 0x14));

    /// <summary>
    /// Gets the item list.
    /// </summary>
    public NostaleList<MapBaseObj> ItemList => new NostaleList<MapBaseObj>(_memory, ReadPtr(Address + 0x18));
    public NostaleList<MapBaseObj> ItemList => new MapBaseList(_memory, ReadPtr(Address + 0x18));

    /// <summary>
    /// Gets the entity that is currently being followed by the player.

A src/Core/NosSmooth.LocalBinding/Structs/UnitManager.cs => src/Core/NosSmooth.LocalBinding/Structs/UnitManager.cs +66 -0
@@ 0,0 1,66 @@
//
//  UnitManager.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.Errors;
using NosSmooth.LocalBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using Reloaded.Memory.Sources;
using Remora.Results;

namespace NosSmooth.LocalBinding.Structs;

/// <summary>
/// An object used for PacketSend and PacketReceive functions.
/// </summary>
public class UnitManager : NostaleObject
{
        /// <summary>
    /// Create <see cref="PlayerManager"/> instance.
    /// </summary>
    /// <param name="nosBrowserManager">The NosTale process browser.</param>
    /// <param name="options">The options.</param>
    /// <returns>The player manager or an error.</returns>
    public static Result<UnitManager> Create(NosBrowserManager nosBrowserManager, UnitManagerOptions options)
    {
        var unitObjectAddress = nosBrowserManager.Scanner.FindPattern(options.UnitManagerPattern);
        if (!unitObjectAddress.Found)
        {
            return new BindingNotFoundError(options.UnitManagerPattern, "UnitManager");
        }

        if (nosBrowserManager.Process.MainModule is null)
        {
            return new NotFoundError("Cannot find the main module of the target process.");
        }

        var staticAddress = (int)(nosBrowserManager.Process.MainModule.BaseAddress + unitObjectAddress.Offset);
        return new UnitManager(nosBrowserManager.Memory, staticAddress, options.UnitManagerOffsets);
    }

    private readonly IMemory _memory;
    private readonly int _staticUnitManagerAddress;
    private readonly int[] _optionsUnitManagerOffsets;

    /// <summary>
    /// Initializes a new instance of the <see cref="UnitManager"/> class.
    /// </summary>
    /// <param name="memory">The memory.</param>
    /// <param name="staticUnitManagerAddress">The pointer to the beginning of the player manager structure.</param>
    /// <param name="optionsUnitManagerOffsets">The unit manager offsets.</param>
    public UnitManager(IMemory memory, int staticUnitManagerAddress, int[] optionsUnitManagerOffsets)
        : base(memory, (nuint)staticUnitManagerAddress)
    {
        _memory = memory;
        _staticUnitManagerAddress = staticUnitManagerAddress;
        _optionsUnitManagerOffsets = optionsUnitManagerOffsets;
    }

    /// <summary>
    /// Gets the address to the player manager.
    /// </summary>
    public override nuint Address => _memory.FollowStaticAddressOffsets
        (_staticUnitManagerAddress, _optionsUnitManagerOffsets);
}
\ No newline at end of file

M src/Core/NosSmooth.LocalClient/CommandHandlers/Attack/AttackCommandHandler.cs => src/Core/NosSmooth.LocalClient/CommandHandlers/Attack/AttackCommandHandler.cs +6 -5
@@ 10,6 10,7 @@ using NosSmooth.Core.Commands.Attack;
using NosSmooth.Core.Commands.Control;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;


@@ 23,7 24,7 @@ public class AttackCommandHandler : ICommandHandler<AttackCommand>
{
    private readonly INostaleClient _nostaleClient;
    private readonly NosThreadSynchronizer _synchronizer;
    private readonly UnitManagerBinding _unitManagerBinding;
    private readonly IEntityFocusHook _entityFocusHook;
    private readonly SceneManager _sceneManager;

    /// <summary>


@@ 31,19 32,19 @@ public class AttackCommandHandler : ICommandHandler<AttackCommand>
    /// </summary>
    /// <param name="nostaleClient">The NosTale client.</param>
    /// <param name="synchronizer">The thread synchronizer.</param>
    /// <param name="unitManagerBinding">The unit manager binding.</param>
    /// <param name="entityFocusHook">The entity focus hook.</param>
    /// <param name="sceneManager">The scene manager.</param>
    public AttackCommandHandler
    (
        INostaleClient nostaleClient,
        NosThreadSynchronizer synchronizer,
        UnitManagerBinding unitManagerBinding,
        IEntityFocusHook entityFocusHook,
        SceneManager sceneManager
    )
    {
        _nostaleClient = nostaleClient;
        _synchronizer = synchronizer;
        _unitManagerBinding = unitManagerBinding;
        _entityFocusHook = entityFocusHook;
        _sceneManager = sceneManager;
    }



@@ 55,7 56,7 @@ public class AttackCommandHandler : ICommandHandler<AttackCommand>
            var entityResult = _sceneManager.FindEntity(command.TargetId.Value);
            if (entityResult.IsDefined(out var entity))
            {
                _synchronizer.EnqueueOperation(() => _unitManagerBinding.FocusEntity(entity));
                _synchronizer.EnqueueOperation(() => _entityFocusHook.WrapperFunction(entity));
            }
        }


M src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PetWalkCommandHandler.cs => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PetWalkCommandHandler.cs +25 -8
@@ 9,7 9,10 @@ using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Commands.Control;
using NosSmooth.Core.Commands.Walking;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;


@@ 24,7 27,9 @@ public class PetWalkCommandHandler : ICommandHandler<PetWalkCommand>
    /// </summary>
    public const string PetWalkControlGroup = "PetWalk";

    private readonly PetManagerBinding _petManagerBinding;
    private readonly IPetWalkHook _petWalkHook;
    private readonly PetManagerList _petManagerList;
    private readonly NosThreadSynchronizer _threadSynchronizer;
    private readonly UserActionDetector _userActionDetector;
    private readonly INostaleClient _nostaleClient;
    private readonly WalkCommandHandlerOptions _options;


@@ 32,20 37,26 @@ public class PetWalkCommandHandler : ICommandHandler<PetWalkCommand>
    /// <summary>
    /// Initializes a new instance of the <see cref="PetWalkCommandHandler"/> class.
    /// </summary>
    /// <param name="petManagerBinding">The character object binding.</param>
    /// <param name="petWalkHook">The pet walk hook.</param>
    /// <param name="petManagerList">The pet manager list.</param>
    /// <param name="threadSynchronizer">The thread synchronizer.</param>
    /// <param name="userActionDetector">The user action detector.</param>
    /// <param name="nostaleClient">The nostale client.</param>
    /// <param name="options">The options.</param>
    public PetWalkCommandHandler
    (
        PetManagerBinding petManagerBinding,
        IPetWalkHook petWalkHook,
        PetManagerList petManagerList,
        NosThreadSynchronizer threadSynchronizer,
        UserActionDetector userActionDetector,
        INostaleClient nostaleClient,
        IOptions<WalkCommandHandlerOptions> options
    )
    {
        _options = options.Value;
        _petManagerBinding = petManagerBinding;
        _petWalkHook = petWalkHook;
        _petManagerList = petManagerList;
        _threadSynchronizer = threadSynchronizer;
        _userActionDetector = userActionDetector;
        _nostaleClient = nostaleClient;
    }


@@ 53,17 64,23 @@ public class PetWalkCommandHandler : ICommandHandler<PetWalkCommand>
    /// <inheritdoc/>
    public async Task<Result> HandleCommand(PetWalkCommand command, CancellationToken ct = default)
    {
        if (_petManagerBinding.PetManagerList.Length < command.PetSelector + 1)
        if (_petManagerList.Length < command.PetSelector + 1)
        {
            return new NotFoundError("Could not find the pet using the given selector.");
        }
        var petManager = _petManagerBinding.PetManagerList[command.PetSelector];
        var petManager = _petManagerList[command.PetSelector];

        var handler = new ControlCommandWalkHandler
        (
            _nostaleClient,
            async (x, y, ct) => await _userActionDetector.NotUserActionAsync
                (() => _petManagerBinding.PetWalk(command.PetSelector, x, y), ct),
            async (x, y, ct) =>
                await _threadSynchronizer.SynchronizeAsync
                (
                    () => _userActionDetector.NotUserAction<Result<bool>>
                    (
                        () => _petWalkHook.WrapperFunction(petManager, (ushort)x, (ushort)y)
                    )
                ),
            petManager,
            _options
        );

M src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PlayerWalkCommandHandler.cs => src/Core/NosSmooth.LocalClient/CommandHandlers/Walk/PlayerWalkCommandHandler.cs +22 -7
@@ 10,7 10,10 @@ using NosSmooth.Core.Commands;
using NosSmooth.Core.Commands.Control;
using NosSmooth.Core.Commands.Walking;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.LocalClient.CommandHandlers.Walk;


@@ 25,7 28,9 @@ public class PlayerWalkCommandHandler : ICommandHandler<PlayerWalkCommand>
    /// </summary>
    public const string PlayerWalkControlGroup = "PlayerWalk";

    private readonly PlayerManagerBinding _playerManagerBinding;
    private readonly PlayerManager _playerManager;
    private readonly IPlayerWalkHook _playerWalkHook;
    private readonly NosThreadSynchronizer _threadSynchronizer;
    private readonly UserActionDetector _userActionDetector;
    private readonly INostaleClient _nostaleClient;
    private readonly WalkCommandHandlerOptions _options;


@@ 33,20 38,26 @@ public class PlayerWalkCommandHandler : ICommandHandler<PlayerWalkCommand>
    /// <summary>
    /// Initializes a new instance of the <see cref="PlayerWalkCommandHandler"/> class.
    /// </summary>
    /// <param name="playerManagerBinding">The character object binding.</param>
    /// <param name="playerManager">The player manager.</param>
    /// <param name="playerWalkHook">The player walk hook.</param>
    /// <param name="threadSynchronizer">The thread synchronizer.</param>
    /// <param name="userActionDetector">The user action detector.</param>
    /// <param name="nostaleClient">The nostale client.</param>
    /// <param name="options">The options.</param>
    public PlayerWalkCommandHandler
    (
        PlayerManagerBinding playerManagerBinding,
        PlayerManager playerManager,
        IPlayerWalkHook playerWalkHook,
        NosThreadSynchronizer threadSynchronizer,
        UserActionDetector userActionDetector,
        INostaleClient nostaleClient,
        IOptions<WalkCommandHandlerOptions> options
    )
    {
        _options = options.Value;
        _playerManagerBinding = playerManagerBinding;
        _playerManager = playerManager;
        _playerWalkHook = playerWalkHook;
        _threadSynchronizer = threadSynchronizer;
        _userActionDetector = userActionDetector;
        _nostaleClient = nostaleClient;
    }


@@ 57,9 68,13 @@ public class PlayerWalkCommandHandler : ICommandHandler<PlayerWalkCommand>
        var handler = new ControlCommandWalkHandler
        (
            _nostaleClient,
            async (x, y, ct) =>
                await _userActionDetector.NotUserWalkAsync(_playerManagerBinding, x, y, ct),
            _playerManagerBinding.PlayerManager,
            async (x, y, ct)
                => await _threadSynchronizer.SynchronizeAsync
                (
                    () => _userActionDetector.NotUserWalk(_playerWalkHook, x, y),
                    ct
                ),
            _playerManager,
            _options
        );


M src/Core/NosSmooth.LocalClient/NostaleLocalClient.cs => src/Core/NosSmooth.LocalClient/NostaleLocalClient.cs +18 -24
@@ 14,6 14,7 @@ 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;


@@ 34,9 35,7 @@ namespace NosSmooth.LocalClient;
public class NostaleLocalClient : BaseNostaleClient
{
    private readonly NosThreadSynchronizer _synchronizer;
    private readonly NetworkBinding _networkBinding;
    private readonly PlayerManagerBinding _playerManagerBinding;
    private readonly PetManagerBinding _petManagerBinding;
    private readonly IHookManager _hookManager;
    private readonly ControlCommands _controlCommands;
    private readonly IPacketSerializer _packetSerializer;
    private readonly PacketHandler _packetHandler;


@@ 51,9 50,7 @@ public class NostaleLocalClient : BaseNostaleClient
    /// Initializes a new instance of the <see cref="NostaleLocalClient"/> class.
    /// </summary>
    /// <param name="synchronizer">The thread synchronizer.</param>
    /// <param name="networkBinding">The network binding.</param>
    /// <param name="playerManagerBinding">The player manager binding.</param>
    /// <param name="petManagerBinding">The pet manager binding.</param>
    /// <param name="hookManager">The hook manager.</param>
    /// <param name="controlCommands">The control commands.</param>
    /// <param name="commandProcessor">The command processor.</param>
    /// <param name="packetSerializer">The packet serializer.</param>


@@ 65,9 62,7 @@ public class NostaleLocalClient : BaseNostaleClient
    public NostaleLocalClient
    (
        NosThreadSynchronizer synchronizer,
        NetworkBinding networkBinding,
        PlayerManagerBinding playerManagerBinding,
        PetManagerBinding petManagerBinding,
        IHookManager hookManager,
        ControlCommands controlCommands,
        CommandProcessor commandProcessor,
        IPacketSerializer packetSerializer,


@@ 81,9 76,7 @@ public class NostaleLocalClient : BaseNostaleClient
    {
        _options = options.Value;
        _synchronizer = synchronizer;
        _networkBinding = networkBinding;
        _playerManagerBinding = playerManagerBinding;
        _petManagerBinding = petManagerBinding;
        _hookManager = hookManager;
        _controlCommands = controlCommands;
        _packetSerializer = packetSerializer;
        _packetHandler = packetHandler;


@@ 98,12 91,12 @@ public class NostaleLocalClient : BaseNostaleClient
        _stopRequested = stopRequested;
        _logger.LogInformation("Starting local client");
        _synchronizer.StartSynchronizer();
        _networkBinding.PacketSendCall += SendCallCallback;
        _networkBinding.PacketReceiveCall += ReceiveCallCallback;
        _hookManager.PacketSend.Called += SendCallCallback;
        _hookManager.PacketReceive.Called += ReceiveCallCallback;

        _playerManagerBinding.FollowEntityCall += FollowEntity;
        _playerManagerBinding.WalkCall += Walk;
        _petManagerBinding.PetWalkCall += PetWalk;
        _hookManager.EntityFollow.Called += FollowEntity;
        _hookManager.PlayerWalk.Called += Walk;
        _hookManager.PetWalk.Called += PetWalk;

        try
        {


@@ 114,11 107,12 @@ public class NostaleLocalClient : BaseNostaleClient
            // ignored
        }

        _networkBinding.PacketSendCall -= SendCallCallback;
        _networkBinding.PacketReceiveCall -= ReceiveCallCallback;
        _playerManagerBinding.FollowEntityCall -= FollowEntity;
        _playerManagerBinding.WalkCall -= Walk;
        _petManagerBinding.PetWalkCall -= PetWalk;
        _hookManager.PacketSend.Called -= SendCallCallback;
        _hookManager.PacketReceive.Called -= ReceiveCallCallback;

        _hookManager.EntityFollow.Called -= FollowEntity;
        _hookManager.PlayerWalk.Called -= Walk;
        _hookManager.PetWalk.Called -= PetWalk;

        return Result.FromSuccess();
    }


@@ 187,7 181,7 @@ public class NostaleLocalClient : BaseNostaleClient
    {
        _synchronizer.EnqueueOperation
        (
            () => _networkBinding.SendPacket(packetString)
            () => _hookManager.PacketSend.WrapperFunction(packetString)
        );
        _logger.LogDebug($"Sending client packet {packetString}");
    }


@@ 196,7 190,7 @@ public class NostaleLocalClient : BaseNostaleClient
    {
        _synchronizer.EnqueueOperation
        (
            () => _networkBinding.ReceivePacket(packetString)
            () => _hookManager.PacketReceive.WrapperFunction(packetString)
        );
        _logger.LogDebug($"Receiving client packet {packetString}");
    }

M src/Core/NosSmooth.LocalClient/UserActionDetector.cs => src/Core/NosSmooth.LocalClient/UserActionDetector.cs +52 -6
@@ 5,6 5,7 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Data.SqlTypes;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;


@@ 50,6 51,36 @@ public class UserActionDetector
    /// Execute an action that makes sure walk won't be treated as a user action.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>Return value of the action.</returns>
    public async Task NotUserActionAsync(Action action, CancellationToken ct = default)
    {
        await _semaphore.WaitAsync(ct);
        _handlingDisabled = true;
        action();
        _handlingDisabled = false;
        _semaphore.Release();
    }

    /// <summary>
    /// Execute an action that makes sure walk won't be treated as a user action.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Result NotUserAction(Func<Result> action)
    {
        _semaphore.Wait();
        _handlingDisabled = true;
        var result = action();
        _handlingDisabled = false;
        _semaphore.Release();
        return result;
    }

    /// <summary>
    /// Execute an action that makes sure walk won't be treated as a user action.
    /// </summary>
    /// <param name="action">The action to execute.</param>
    /// <typeparam name="T">The return type.</typeparam>
    /// <returns>Return value of the action.</returns>
    public T NotUserAction<T>(Func<T> action)


@@ 65,23 96,38 @@ public class UserActionDetector
    /// <summary>
    /// Execute walk action and make sure walk won't be treated as a user action.
    /// </summary>
    /// <param name="playerManager">The player manager.</param>
    /// <param name="walkHook">The walk hook.</param>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    /// <returns>The result of the walk.</returns>
    public Result<bool> NotUserWalk(IPlayerWalkHook walkHook, short x, short y)
        => NotUserAction
        (
            () =>
            {
                _lastWalkPosition = ((ushort)x, (ushort)y);
                return walkHook.WrapperFunction((ushort)x, (ushort)y);
            }
        );

    /// <summary>
    /// Execute walk action and make sure walk won't be treated as a user action.
    /// </summary>
    /// <param name="walkHook">The walk hook.</param>
    /// <param name="x">The x coordinate.</param>
    /// <param name="y">The y coordinate.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>The result of the walk.</returns>
    public async Task<Result<bool>> NotUserWalkAsync(PlayerManagerBinding playerManager, short x, short y, CancellationToken ct = default)
    {
        return await NotUserActionAsync
    public async Task<Result<bool>> NotUserWalkAsync(IPlayerWalkHook walkHook, short x, short y, CancellationToken ct = default)
        => await NotUserActionAsync
        (
            () =>
            {
                _lastWalkPosition = ((ushort)x, (ushort)y);
                return playerManager.Walk(x, y);
                return walkHook.WrapperFunction((ushort)x, (ushort)y);
            },
            ct
        );
    }

    /// <summary>
    /// Checks whether the given Walk call

M src/Samples/External/ExternalBrowser/Program.cs => src/Samples/External/ExternalBrowser/Program.cs +13 -2
@@ 8,6 8,7 @@ using System.Diagnostics;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Options;
using NosSmooth.LocalBinding.Structs;

namespace ExternalBrowser;



@@ 25,6 26,8 @@ public class Program
        var playerManagerOptions = new PlayerManagerOptions();
        var sceneManagerOptions = new SceneManagerOptions();
        var petManagerOptions = new PetManagerOptions();
        var networkManagerOptions = new NetworkManagerOptions();
        var unitManagerOptions = new UnitManagerOptions();

        foreach (var argument in arguments)
        {


@@ 45,7 48,14 @@ public class Program
            foreach (var process in processes)
            {
                var externalBrowser = new NosBrowserManager
                    (process, playerManagerOptions, sceneManagerOptions, petManagerOptions);
                (
                    process,
                    playerManagerOptions,
                    sceneManagerOptions,
                    petManagerOptions,
                    networkManagerOptions,
                    unitManagerOptions
                );

                if (!externalBrowser.IsNostaleProcess)
                {


@@ 68,7 78,8 @@ public class Program
                    continue;
                }

                Console.WriteLine($"Player in process {process.Id} is named {externalBrowser.PlayerManager.Player.Name}");
                Console.WriteLine
                    ($"Player in process {process.Id} is named {externalBrowser.PlayerManager.Player.Name}");
            }
        }
    }

M src/Samples/HighLevel/SimplePiiBot/Commands/EntityCommands.cs => src/Samples/HighLevel/SimplePiiBot/Commands/EntityCommands.cs +21 -12
@@ 9,6 9,7 @@ using NosSmooth.Extensions.Combat.Errors;
using NosSmooth.Game;
using NosSmooth.Game.Apis;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Objects;
using NosSmooth.LocalBinding.Structs;
using NosSmooth.Packets.Enums.Chat;


@@ 24,36 25,32 @@ namespace SimplePiiBot.Commands;
public class EntityCommands : CommandGroup
{
    private readonly Game _game;
    private readonly IHookManager _hookManager;
    private readonly NosThreadSynchronizer _synchronizer;
    private readonly UnitManagerBinding _unitManagerBinding;
    private readonly SceneManager _sceneManager;
    private readonly PlayerManagerBinding _playerManagerBinding;
    private readonly FeedbackService _feedbackService;

    /// <summary>
    /// Initializes a new instance of the <see cref="EntityCommands"/> class.
    /// </summary>
    /// <param name="game">The game.</param>
    /// <param name="hookManager">The hook manager.</param>
    /// <param name="synchronizer">The thread synchronizer.</param>
    /// <param name="unitManagerBinding">The scene manager binding.</param>
    /// <param name="sceneManager">The scene manager.</param>
    /// <param name="playerManagerBinding">The character binding.</param>
    /// <param name="feedbackService">The feedback service.</param>
    public EntityCommands
    (
        Game game,
        IHookManager hookManager,
        NosThreadSynchronizer synchronizer,
        UnitManagerBinding unitManagerBinding,
        SceneManager sceneManager,
        PlayerManagerBinding playerManagerBinding,
        FeedbackService feedbackService
    )
    {
        _game = game;
        _hookManager = hookManager;
        _synchronizer = synchronizer;
        _unitManagerBinding = unitManagerBinding;
        _sceneManager = sceneManager;
        _playerManagerBinding = playerManagerBinding;
        _feedbackService = feedbackService;
    }



@@ 117,7 114,11 @@ public class EntityCommands : CommandGroup

        return await _synchronizer.SynchronizeAsync
        (
            () => _unitManagerBinding.FocusEntity(entityResult.Entity),
            () =>
            {
                _hookManager.EntityFocus.WrapperFunction(entityResult.Entity);
                return Result.FromSuccess();
            },
            CancellationToken
        );
    }


@@ 131,14 132,18 @@ public class EntityCommands : CommandGroup
    public async Task<Result> HandleFollowAsync(int entityId)
    {
        var entityResult = _sceneManager.FindEntity(entityId);
        if (!entityResult.IsSuccess)
        if (!entityResult.IsDefined(out var entity))
        {
            return Result.FromError(entityResult);
        }

        return await _synchronizer.SynchronizeAsync
        (
            () => _playerManagerBinding.FollowEntity(entityResult.Entity),
            () =>
            {
                _hookManager.EntityFollow.WrapperFunction(entity);
                return Result.FromSuccess();
            },
            CancellationToken
        );
    }


@@ 152,7 157,11 @@ public class EntityCommands : CommandGroup
    {
        return await _synchronizer.SynchronizeAsync
        (
            () => _playerManagerBinding.UnfollowEntity(),
            () =>
            {
                _hookManager.EntityUnfollow.WrapperFunction();
                return Result.FromSuccess();
            },
            CancellationToken
        );
    }

Do not follow this link