~ruther/NosSmooth.Comms

ecfc8cd7dc9143e9550044f3f5971bf8631f51b7 — Rutherther 2 years ago 8a197a0
feat: add local injectable and injector to allow making connections to nostale processes
30 files changed, 1263 insertions(+), 39 deletions(-)

M NosSmooth.Comms.sln
M src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs
M src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj
M src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs
M src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs
A src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs
M src/Core/NosSmooth.Comms.Core/MessageHandler.cs
M src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj
M src/Core/NosSmooth.Comms.Core/ServerManager.cs
M src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs
A src/Local/NosSmooth.Comms.Inject/CallbackConfig.cs
A src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs
A src/Local/NosSmooth.Comms.Inject/DllMain.cs
A src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs
A src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs
A src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs
A src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs
A src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs
A src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj
A src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs
A src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs
A src/Local/NosSmooth.Comms.Local/Comms.cs
A src/Local/NosSmooth.Comms.Local/CommsInjector.cs
A src/Local/NosSmooth.Comms.Local/Extensions/ServiceCollectionExtensions.cs
A src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs
A src/Local/NosSmooth.Comms.Local/MessageResponders/RawPacketResponder.cs
A src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj
A src/Local/NosSmooth.Comms.LocalData/FocusMessage.cs
A src/Local/NosSmooth.Comms.LocalData/FollowMessage.cs
A src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj
M NosSmooth.Comms.sln => NosSmooth.Comms.sln +2 -2
@@ 2,11 2,11 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Abstractions", "src\Core\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj", "{D3B6C5ED-9291-4215-8F8A-F3530849B896}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Local", "src\Local\NosSmooth.Comms.Local\NosSmooth.Comms.Local.csproj", "{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Inject", "src\Local\NosSmooth.Comms.Inject\NosSmooth.Comms.Inject.csproj", "{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.NamedPipes", "src\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj", "{9621D790-97FC-4E01-BB0C-CA1F33B4C934}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Injector", "src\Local\NosSmooth.Comms.Injector\NosSmooth.Comms.Injector.csproj", "{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Local", "src\Local\NosSmooth.Comms.Local\NosSmooth.Comms.Local.csproj", "{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{EFE65101-4414-4966-813D-90FE3736F6B1}"
EndProject

M src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs +1 -1
@@ 6,4 6,4 @@

namespace NosSmooth.Comms.Data.Messages;

public record HandshakeRequest(bool SendRawPackets, bool SendDeserializedPackets);
\ No newline at end of file
public record HandshakeRequest(string Identification, bool SendRawPackets, bool SendDeserializedPackets);
\ No newline at end of file

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

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
      <PackageReference Include="NosSmooth.Core" Version="3.3.1" />
      <PackageReference Include="NosSmooth.Packets" Version="3.5.0" />
      <PackageReference Include="NosSmooth.PacketSerializer.Abstractions" Version="1.3.0" />
    </ItemGroup>

M src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs => src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs +21 -16
@@ 8,8 8,8 @@ using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Contracts;
using NosSmooth.Packets;
using NosSmooth.PacketSerializer;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using Remora.Results;



@@ 42,40 42,45 @@ public class ClientNostaleClient : INostaleClient
    /// <inheritdoc />
    public async Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new PacketMessage(PacketSource.Client, packet), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
        var messageResponse = await _connection.ContractSendMessage
                (new PacketMessage(PacketSource.Client, packet))
            .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
        return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new RawPacketMessage(PacketSource.Client, packetString), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
        var messageResponse = await _connection.ContractSendMessage
                (new RawPacketMessage(PacketSource.Client, packetString))
            .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
        return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new RawPacketMessage(PacketSource.Server, packetString), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
        var messageResponse = await _connection.ContractSendMessage
                (new RawPacketMessage(PacketSource.Server, packetString))
            .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
        return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new PacketMessage(PacketSource.Server, packet), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
        var messageResponse = await _connection.ContractSendMessage
                (new PacketMessage(PacketSource.Server, packet))
            .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
        return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
    }

    /// <inheritdoc />
    public async Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default)
    {
        var messageResponse = await _connection.SendMessageAsync
            (new CommandMessage(command), ct);
        return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
        var messageResponse = await _connection.ContractSendMessage
                (new CommandMessage(command))
            .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
        return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
    }
}
\ No newline at end of file

M src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs => src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs +55 -2
@@ 4,6 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.

using System.Data;
using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;


@@ 51,9 52,20 @@ public class ConnectionHandler
        _messageHandler = messageHandler;
        _options = options.Value;
        _logger = logger;
        Id = Guid.NewGuid();
    }

    /// <summary>
    /// Gets the id of the connection.
    /// </summary>
    public Guid Id { get; }

    /// <summary>
    /// The connection has been closed.
    /// </summary>
    public event EventHandler? Closed;

    /// <summary>
    /// Gets the connection.
    /// </summary>
    public IConnection Connection => _connection;


@@ 86,19 98,19 @@ public class ConnectionHandler
    private async Task<Result> HandlerTask(CancellationToken ct)
    {
        using var reader = new MessagePackStreamReader(_connection.ReadStream, true);
        while (!ct.IsCancellationRequested)
        while (!ct.IsCancellationRequested && _connection.State == ConnectionState.Open)
        {
            try
            {
                var read = await reader.ReadAsync(ct);
                if (!read.HasValue)
                {
                    _logger.LogWarning("Message not read? ...");
                    continue;
                }

                var message = MessagePackSerializer.Typeless.Deserialize
                    (read.Value, _options, ct);

                var result = await _messageHandler.HandleMessageAsync(this, message, ct);

                if (!result.IsSuccess)


@@ 113,6 125,7 @@ public class ConnectionHandler
        }

        _connection.Disconnect();
        Closed?.Invoke(this, EventArgs.Empty);
        return Result.FromSuccess();
    }



@@ 120,6 133,46 @@ public class ConnectionHandler
    /// Create a contract for sending a message,
    /// <see cref="ResponseResult"/> will be returned back.
    /// </summary>
    /// <param name="handshake">The handshake request.</param>
    /// <typeparam name="TMessage">The type of the message.</typeparam>
    /// <returns>A contract representing send message operation.</returns>
    /// <exception cref="InvalidOperationException">Thrown in case contract is created on the server. Clients do not send responses.</exception>
    public IContract<HandshakeResponse, DefaultStates> ContractHanshake(HandshakeRequest handshake)
    {
        if (_contractor is null)
        {
            throw new InvalidOperationException
            (
                "Contracting is not supported, the other side does not send responses. Only server sends responses back."
            );
        }

        return new ContractBuilder<HandshakeResponse, DefaultStates, NoErrors>(_contractor, DefaultStates.None)
            .SetMoveAction
            (
                DefaultStates.None,
                async (a, ct) =>
                {
                    var result = await SendMessageAsync(handshake, ct);
                    if (!result.IsDefined(out _))
                    {
                        return Result<bool>.FromError(result);
                    }

                    return true;
                },
                DefaultStates.Requested
            )
            .SetMoveFilter<HandshakeResponse>
                (DefaultStates.Requested, DefaultStates.ResponseObtained)
            .SetFillData<HandshakeResponse>(DefaultStates.ResponseObtained, r => r)
            .Build();
    }

    /// <summary>
    /// Create a contract for sending a message,
    /// <see cref="ResponseResult"/> will be returned back.
    /// </summary>
    /// <param name="message">The message.</param>
    /// <typeparam name="TMessage">The type of the message.</typeparam>
    /// <returns>A contract representing send message operation.</returns>

A src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs => src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs +14 -0
@@ 0,0 1,14 @@
//
//  MessageHandlerNotFoundError.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.Comms.Core.Errors;

/// <summary>
/// No message handler was found for the received message.
/// </summary>
public record MessageHandlerNotFoundError() : ResultError("Message handler for the given message was not found.");
\ No newline at end of file

M src/Core/NosSmooth.Comms.Core/MessageHandler.cs => src/Core/NosSmooth.Comms.Core/MessageHandler.cs +24 -3
@@ 4,13 4,16 @@
//  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.Contracts;
using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NosSmooth.Comms.Core.Errors;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.Core.Contracts;
using Remora.Results;

namespace NosSmooth.Comms.Core;


@@ 67,9 70,20 @@ public class MessageHandler

    private async Task<Result> GenericHandleMessageAsync<TMessage>
        (ConnectionHandler connection, MessageWrapper<TMessage> wrappedMessage, CancellationToken ct)
        where TMessage : notnull
    {
        var data = wrappedMessage.Data;

        var contractor = _services.GetService<Contractor>();
        if (contractor is not null)
        {
            var contractorResult = await contractor.Update(wrappedMessage.Data, ct);
            if (!contractorResult.IsSuccess)
            {
                return contractorResult;
            }
        }

        await using var scope = _services.CreateAsyncScope();
        var injector = scope.ServiceProvider.GetRequiredService<ConnectionInjector>();
        injector.ConnectionHandler = connection;


@@ 77,7 91,8 @@ public class MessageHandler

        var responders = scope.ServiceProvider
            .GetServices<IMessageResponder<TMessage>>()
            .Select(x => x.Respond(data, ct));
            .Select(x => x.Respond(data, ct))
            .ToArray();

        var results = (await Task.WhenAll(responders))
            .Where(x => !x.IsSuccess)


@@ 93,11 108,17 @@ public class MessageHandler

        if (_respond && wrappedMessage.Data is not ResponseResult)
        {
            var response = new ResponseResult(wrappedMessage.MessageId, result);
            var responseResult = result;
            if (responders.Length == 0)
            {
                responseResult = new MessageHandlerNotFoundError();
            }

            var response = new ResponseResult(wrappedMessage.MessageId, responseResult);
            var sentMessageResult = await connection.SendMessageAsync(response, ct);
            if (!sentMessageResult.IsSuccess)
            {
                results.Add(sentMessageResult);
                results.Add(Result.FromError(sentMessageResult));
                result = results.Count switch
                {
                    0 => Result.FromSuccess(),

M src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj => src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj +1 -0
@@ 22,6 22,7 @@
    </ItemGroup>

    <ItemGroup>
      <PackageReference Include="MessagePack" Version="2.4.59" />
      <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
      <PackageReference Include="NosSmooth.Core" Version="3.3.1" />
    </ItemGroup>

M src/Core/NosSmooth.Comms.Core/ServerManager.cs => src/Core/NosSmooth.Comms.Core/ServerManager.cs +65 -4
@@ 7,7 7,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Extensions;
using Remora.Results;



@@ 24,6 23,7 @@ public class ServerManager
    private readonly ILogger<ServerManager> _logger;
    private readonly ILogger<ConnectionHandler> _handlerLogger;
    private readonly List<ConnectionHandler> _connectionHandlers;
    private readonly ReaderWriterLockSlim _readerWriterLock;
    private Task<Result>? _task;
    private CancellationTokenSource? _ctSource;



@@ 46,6 46,7 @@ public class ServerManager
    {
        _server = server;
        _connectionHandlers = new List<ConnectionHandler>();
        _readerWriterLock = new ReaderWriterLockSlim();
        _messageHandler = messageHandler;
        _options = options;
        _logger = logger;


@@ 53,6 54,25 @@ public class ServerManager
    }

    /// <summary>
    /// Gets connection handlers.
    /// </summary>
    public IReadOnlyList<ConnectionHandler> ConnectionHandlers
    {
        get
        {
            _readerWriterLock.EnterReadLock();
            try
            {
                return _connectionHandlers.ToArray();
            }
            finally
            {
                _readerWriterLock.ExitReadLock();
            }
        }
    }

    /// <summary>
    /// Run the manager and await the task.
    /// </summary>
    /// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>


@@ 131,14 151,45 @@ public class ServerManager
                continue;
            }

            var handler = new ConnectionHandler(null, connection, _messageHandler, _options, _handlerLogger);
            _connectionHandlers.Add(handler);
            var handler = new ConnectionHandler
            (
                null,
                connection,
                _messageHandler,
                _options,
                _handlerLogger
            );
            _readerWriterLock.EnterWriteLock();

            try
            {
                _connectionHandlers.Add(handler);
            }
            finally
            {
                _readerWriterLock.ExitWriteLock();
            }

            handler.Closed += (o, e) =>
            {
                _logger.LogInformation("A connection ({ConnectionId}) has been closed", handler.Id);
                _readerWriterLock.EnterWriteLock();
                try
                {
                    _connectionHandlers.Remove(handler);
                }
                finally
                {
                    _readerWriterLock.ExitWriteLock();
                }
            };

            _logger.LogInformation("A connection ({ConnectionId}) has been established", handler.Id);
            handler.StartHandler(_ctSource.Token);
        }

        List<IResult> errors = new List<IResult>();
        foreach (var handler in _connectionHandlers)
        foreach (var handler in ConnectionHandlers)
        {
            var handlerResult = await handler.RunHandlerAsync(_ctSource.Token);



@@ 148,6 199,16 @@ public class ServerManager
            }
        }

        _readerWriterLock.EnterWriteLock();
        try
        {
            _connectionHandlers.Clear();
        }
        finally
        {
            _readerWriterLock.ExitWriteLock();
        }

        _server.Close();
        return errors.Count switch
        {

M src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs => src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs +35 -11
@@ 6,6 6,7 @@

using System.Data;
using System.IO.Pipes;
using System.Xml;
using NosSmooth.Comms.Data;
using Remora.Results;



@@ 38,9 39,14 @@ public class NamedPipeServer : IServer
        get
        {
            _readerWriterLock.EnterReadLock();
            var connections = new List<IConnection>(_connections);
            _readerWriterLock.ExitReadLock();
            return connections.AsReadOnly();
            try
            {
                return _connections.ToArray();
            }
            finally
            {
                _readerWriterLock.ExitReadLock();
            }
        }
    }



@@ 65,8 71,14 @@ public class NamedPipeServer : IServer

        var connection = new NamedPipeConnection(this, serverStream);
        _readerWriterLock.EnterWriteLock();
        _connections.Add(connection);
        _readerWriterLock.ExitWriteLock();
        try
        {
            _connections.Add(connection);
        }
        finally
        {
            _readerWriterLock.ExitWriteLock();
        }

        return connection;
    }


@@ 83,8 95,15 @@ public class NamedPipeServer : IServer
    public void Close()
    {
        _readerWriterLock.EnterReadLock();
        var connections = new List<IConnection>(_connections);
        _readerWriterLock.ExitReadLock();
        IReadOnlyList<IConnection> connections;
        try
        {
            connections = new List<IConnection>(_connections);
        }
        finally
        {
            _readerWriterLock.ExitReadLock();
        }

        foreach (var connection in connections)
        {


@@ 105,7 124,7 @@ public class NamedPipeServer : IServer
            _serverStream = serverStream;
        }

        public ConnectionState State { get; private set; } = ConnectionState.Open;
        public ConnectionState State => _serverStream.IsConnected ? ConnectionState.Open : ConnectionState.Closed;

        public Stream ReadStream => _serverStream;



@@ 115,11 134,16 @@ public class NamedPipeServer : IServer
        {
            _serverStream.Disconnect();
            _serverStream.Close();
            State = ConnectionState.Closed;

            _server._readerWriterLock.EnterWriteLock();
            _server._connections.Remove(this);
            _server._readerWriterLock.ExitWriteLock();
            try
            {
                _server._connections.Remove(this);
            }
            finally
            {
                _server._readerWriterLock.ExitWriteLock();
            }
        }
    }
}
\ No newline at end of file

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

namespace NosSmooth.Comms.Inject;

public record CallbackConfig(bool SendRawPackets, bool SendDeserializedPackets);
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs => src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs +46 -0
@@ 0,0 1,46 @@
//
//  CallbackConfigRepository.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 NosSmooth.Comms.Core;

namespace NosSmooth.Comms.Inject;

/// <summary>
/// A repository containing configurations for given connection handlers.
/// </summary>
public class CallbackConfigRepository
{
    private readonly ConcurrentDictionary<ConnectionHandler, CallbackConfig> _configs;

    /// <summary>
    /// Initializes a new instance of the <see cref="CallbackConfigRepository"/> class.
    /// </summary>
    public CallbackConfigRepository()
    {
        _configs = new ConcurrentDictionary<ConnectionHandler, CallbackConfig>();
    }

    /// <summary>
    /// Get config of the given connection, or default.
    /// </summary>
    /// <param name="connection">The connection to get config of.</param>
    /// <returns>A config for the connection.</returns>
    public CallbackConfig GetConfig(ConnectionHandler connection)
    {
        return _configs.GetValueOrDefault(connection, new CallbackConfig(false, false));
    }

    /// <summary>
    /// Set config of the given connection.
    /// </summary>
    /// <param name="connection">The connection to set config.</param>
    /// <param name="config">The config to set.</param>
    public void SetConfig(ConnectionHandler connection, CallbackConfig config)
    {
        _configs.AddOrUpdate(connection, _ => config, (a, b) => config);
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/DllMain.cs => src/Local/NosSmooth.Comms.Inject/DllMain.cs +129 -0
@@ 0,0 1,129 @@
//
//  DllMain.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 System.Runtime.InteropServices;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NosSmooth.Comms.Core;
using NosSmooth.Comms.Core.Extensions;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Inject.MessageResponders;
using NosSmooth.Comms.Inject.PacketResponders;
using NosSmooth.Comms.NamedPipes;
using NosSmooth.Comms.NamedPipes.Extensions;
using NosSmooth.Core.Extensions;
using NosSmooth.Extensions.SharedBinding.Extensions;
using NosSmooth.LocalClient.Extensions;
using Remora.Results;

namespace NosSmooth.Comms.Inject;

/// <summary>
/// A main entrypoint to NosSmooth local communications.
/// </summary>
public class DllMain
{
    private static IHost? _host;

    /// <summary>
    /// Allocate console.
    /// </summary>
    /// <returns>Whether the operation was successful.</returns>
    [DllImport("kernel32")]
    public static extern bool AllocConsole();

    /// <summary>
    /// Enable named pipes server.
    /// </summary>
    [UnmanagedCallersOnly(EntryPoint = "EnableNamedPipes")]
    public static void EnableNamedPipes()
    {
        Main
        (
            host =>
            {
                var manager = host.Services.GetRequiredService<ServerManager>();
                return manager.RunManagerAsync(host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping);
            }
        );
    }

    private static void Main(Func<IHost, Task<Result>> host)
    {
        AllocConsole();
        new Thread
        (
            () =>
            {
                try
                {
                    MainEntry(host).GetAwaiter().GetResult();
                }
                catch (Exception e)
                {
                    Console.WriteLine(e.ToString());
                }
            }
        ).Start();
    }

    /// <summary>
    /// The entrypoint method.
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
    private static async Task MainEntry(Func<IHost, Task<Result>> host)
    {
        if (_host is not null)
        {
            var result = await host(_host);
            if (!result.IsSuccess)
            {
                _host.Services.GetRequiredService<ILogger<DllMain>>().LogResultError(result);
            }
            return;
        }

        _host = Host.CreateDefaultBuilder()
            .UseConsoleLifetime()
            .ConfigureLogging
            (
                b =>
                {
                    b
                        .ClearProviders()
                        .AddConsole();
                }
            )
            .ConfigureServices
            (
                s =>
                {
                    s
                        .AddSingleton<CallbackConfigRepository>()
                        .AddNostaleCore()
                        .AddLocalClient()
                        .ShareNosSmooth()
                        .AddNamedPipeServer(p => $"NosSmooth_{Process.GetCurrentProcess().Id}")
                        .AddPacketResponder<EveryPacketResponder>()
                        .AddServerHandling()
                        .AddMessageResponder<CommandResponder>()
                        .AddMessageResponder<FocusResponder>()
                        .AddMessageResponder<FollowResponder>()
                        .AddMessageResponder<HandshakeResponder>()
                        .AddMessageResponder<PacketResponder>();
                    s.AddHostedService<NosSmoothService>();
                }
            ).Build();

        await _host.StartAsync();
        var hostTask = _host.RunAsync();
        var serverTask = host(_host);

        await Task.WhenAll(hostTask, serverTask);
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs +34 -0
@@ 0,0 1,34 @@
//
//  CommandResponder.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.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.Core.Client;
using Remora.Results;

namespace NosSmooth.Comms.Inject.MessageResponders;

/// <summary>
/// A responder to <see cref="CommandMessage"/>.
/// </summary>
public class CommandResponder : IMessageResponder<CommandMessage>
{
    private readonly INostaleClient _client;

    /// <summary>
    /// Initializes a new instance of the <see cref="CommandResponder"/> class.
    /// </summary>
    /// <param name="client">The client.</param>
    public CommandResponder(INostaleClient client)
    {
        _client = client;

    }

    /// <inheritdoc />
    public Task<Result> Respond(CommandMessage message, CancellationToken ct = default)
        => _client.SendCommandAsync(message.Command, ct);
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs +63 -0
@@ 0,0 1,63 @@
//
//  FocusResponder.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.Comms.Data.Responders;
using NosSmooth.Comms.LocalData;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.Comms.Inject.MessageResponders;

/// <summary>
/// A resopnder to <see cref="FocusResponder"/>.
/// </summary>
public class FocusResponder : IMessageResponder<FocusMessage>
{
    private readonly NosBrowserManager _browserManager;
    private readonly NosThreadSynchronizer _synchronizer;
    private readonly IEntityFocusHook _focusHook;

    /// <summary>
    /// Initializes a new instance of the <see cref="FocusResponder"/> class.
    /// </summary>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="synchronizer">The synchronizer.</param>
    /// <param name="focusHook">The focus hook.</param>
    public FocusResponder
        (NosBrowserManager browserManager, NosThreadSynchronizer synchronizer, IEntityFocusHook focusHook)
    {
        _browserManager = browserManager;
        _synchronizer = synchronizer;
        _focusHook = focusHook;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(FocusMessage message, CancellationToken ct = default)
    {
        MapBaseObj? entity = null;
        if (message.EntityId is not null)
        {
            var entityResult = _browserManager.SceneManager.FindEntity(message.EntityId.Value);

            if (!entityResult.IsDefined(out entity))
            {
                return Result.FromError(new NotFoundError($"Entity with id {message.EntityId} not found."));
            }
        }

        return await _synchronizer.SynchronizeAsync
        (
            () =>
            {
                _focusHook.WrapperFunction(entity);
                return Result.FromSuccess();
            },
            ct
        );
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs +70 -0
@@ 0,0 1,70 @@
//
//  FollowResponder.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.Comms.Data.Responders;
using NosSmooth.Comms.LocalData;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Hooks;
using NosSmooth.LocalBinding.Structs;
using Remora.Results;

namespace NosSmooth.Comms.Inject.MessageResponders;

/// <summary>
/// A responder to <see cref="FollowMessage"/>.
/// </summary>
public class FollowResponder : IMessageResponder<FollowMessage>
{
    private readonly NosBrowserManager _browserManager;
    private readonly NosThreadSynchronizer _synchronizer;
    private readonly IHookManager _hookManager;

    /// <summary>
    /// Initializes a new instance of the <see cref="FollowResponder"/> class.
    /// </summary>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="synchronizer">The synchronizer.</param>
    /// <param name="hookManager">The hook manager.</param>
    public FollowResponder
        (NosBrowserManager browserManager, NosThreadSynchronizer synchronizer, IHookManager hookManager)
    {
        _browserManager = browserManager;
        _synchronizer = synchronizer;
        _hookManager = hookManager;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(FollowMessage message, CancellationToken ct = default)
    {
        MapBaseObj? entity = null;
        if (message.EntityId is not null)
        {
            var entityResult = _browserManager.SceneManager.FindEntity(message.EntityId.Value);

            if (!entityResult.IsDefined(out entity))
            {
                return Result.FromError(new NotFoundError($"Entity with id {message.EntityId} not found."));
            }
        }

        return await _synchronizer.SynchronizeAsync
        (
            () =>
            {
                if (entity is null)
                {
                    _hookManager.EntityUnfollow.WrapperFunction();
                }
                else
                {
                    _hookManager.EntityFollow.WrapperFunction(entity);
                }
                return Result.FromSuccess();
            },
            ct
        );
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs +77 -0
@@ 0,0 1,77 @@
//
//  HandshakeResponder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.Extensions.Logging;
using NosSmooth.Comms.Core;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.LocalBinding;
using Remora.Results;

namespace NosSmooth.Comms.Inject.MessageResponders;

/// <summary>
/// A responder to <see cref="HandshakeRequest"/>.
/// </summary>
public class HandshakeResponder : IMessageResponder<HandshakeRequest>
{
    private readonly NosBrowserManager _browserManager;
    private readonly ConnectionHandler _connectionHandler;
    private readonly CallbackConfigRepository _config;
    private readonly ILogger<HandshakeResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="HandshakeResponder"/> class.
    /// </summary>
    /// <param name="browserManager">The browser manager.</param>
    /// <param name="connectionHandler">The connection handler.</param>
    /// <param name="config">The config.</param>
    /// <param name="logger">The logger.</param>
    public HandshakeResponder
    (
        NosBrowserManager browserManager,
        ConnectionHandler connectionHandler,
        CallbackConfigRepository config,
        ILogger<HandshakeResponder> logger
    )
    {
        _browserManager = browserManager;
        _connectionHandler = connectionHandler;
        _config = config;
        _logger = logger;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(HandshakeRequest message, CancellationToken ct = default)
    {
        var config = new CallbackConfig(message.SendRawPackets, message.SendDeserializedPackets);
        _config.SetConfig(_connectionHandler, config);

        string? playerName = null;
        long? playerId = null;

        if (_browserManager.IsInGame)
        {
            playerName = _browserManager.PlayerManager.Player.Name;
            playerId = _browserManager.PlayerManager.PlayerId;
        }

        var result = await _connectionHandler.SendMessageAsync
        (
            new HandshakeResponse(playerId, playerName),
            ct
        );

        _logger.LogInformation
        (
            "Handshaked with {Identification}! (connection {ConnectionID})",
            message.Identification,
            _connectionHandler.Id
        );

        return result.IsSuccess ? Result.FromSuccess() : Result.FromError(result);
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs +52 -0
@@ 0,0 1,52 @@
//
//  PacketResponder.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.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.Core.Client;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using Remora.Results;

namespace NosSmooth.Comms.Inject.MessageResponders;

/// <summary>
/// A responder to <see cref="RawPacketMessage"/> and <see cref="PacketMessage"/>.
/// </summary>
public class PacketResponder : IMessageResponder<RawPacketMessage>, IMessageResponder<PacketMessage>
{
    private readonly INostaleClient _client;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketResponder"/> class.
    /// </summary>
    /// <param name="client">The NosTale client.</param>
    public PacketResponder(INostaleClient client)
    {
        _client = client;
    }

    /// <inheritdoc />
    public Task<Result> Respond(RawPacketMessage message, CancellationToken ct = default)
    {
        if (message.Source == PacketSource.Client)
        {
            return _client.SendPacketAsync(message.Packet, ct);
        }

        return _client.ReceivePacketAsync(message.Packet, ct);
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketMessage message, CancellationToken ct = default)
    {
        if (message.Source == PacketSource.Client)
        {
            return _client.SendPacketAsync(message.Packet, ct);
        }

        return _client.ReceivePacketAsync(message.Packet, ct);
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj => src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj +26 -0
@@ 0,0 1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
        <EnableDynamicLoading>true</EnableDynamicLoading>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
      <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
      <PackageReference Include="NosSmooth.Extensions.SharedBinding" Version="0.0.1" />
      <PackageReference Include="NosSmooth.LocalBinding" Version="1.0.0" />
      <PackageReference Include="NosSmooth.LocalClient" Version="1.0.0" />
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\..\Core\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj" />
      <ProjectReference Include="..\..\Core\NosSmooth.Comms.Core\NosSmooth.Comms.Core.csproj" />
      <ProjectReference Include="..\..\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj" />
      <ProjectReference Include="..\NosSmooth.Comms.LocalData\NosSmooth.Comms.LocalData.csproj" />
    </ItemGroup>

</Project>

A src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs => src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs +79 -0
@@ 0,0 1,79 @@
//
//  NosSmoothService.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.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NosSmooth.Core.Client;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalBinding;
using NosSmooth.PacketSerializer.Extensions;
using NosSmooth.PacketSerializer.Packets;

namespace NosSmooth.Comms.Inject;

/// <summary>
/// Nostale client runner.
/// </summary>
public class NosSmoothService : BackgroundService
{
    private readonly IServiceProvider _services;
    private readonly IPacketTypesRepository _packetTypesRepository;
    private readonly NosBindingManager _bindingManager;
    private readonly IHostLifetime _lifetime;
    private readonly ILogger<NosSmoothService> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="NosSmoothService"/> class.
    /// </summary>
    /// <param name="services">The services.</param>
    /// <param name="packetTypesRepository">The packet types repository.</param>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="lifetime">The lifetime.</param>
    /// <param name="logger">The logger.</param>
    public NosSmoothService
    (
        IServiceProvider services,
        IPacketTypesRepository packetTypesRepository,
        NosBindingManager bindingManager,
        IHostLifetime lifetime,
        ILogger<NosSmoothService> logger
    )
    {
        _services = services;
        _packetTypesRepository = packetTypesRepository;
        _bindingManager = bindingManager;
        _lifetime = lifetime;
        _logger = logger;
    }

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var packetResult = _packetTypesRepository.AddDefaultPackets();
        if (!packetResult.IsSuccess)
        {
            _logger.LogResultError(packetResult);
            return;
        }

        var bindingResult = _bindingManager.Initialize();
        if (!bindingResult.IsSuccess)
        {
            _logger.LogResultError(bindingResult);
            return;
        }

        var nostaleClient = _services.GetRequiredService<INostaleClient>();
        var runResult = await nostaleClient.RunAsync(stoppingToken);
        if (!runResult.IsSuccess)
        {
            _logger.LogResultError(runResult);
            await _lifetime.StopAsync(default);
        }
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs => src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs +70 -0
@@ 0,0 1,70 @@
//
//  EveryPacketResponder.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.Comms.Core;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Packets;
using NosSmooth.Packets;
using Remora.Results;

namespace NosSmooth.Comms.Inject.PacketResponders;

/// <inheritdoc />
public class EveryPacketResponder : IEveryPacketResponder
{
    private readonly CallbackConfigRepository _config;
    private readonly ServerManager _serverManager;

    /// <summary>
    /// Initializes a new instance of the <see cref="EveryPacketResponder"/> class.
    /// </summary>
    /// <param name="config">The configuration.</param>
    /// <param name="serverManager">The server manager.</param>
    public EveryPacketResponder(CallbackConfigRepository config, ServerManager serverManager)
    {
        _config = config;
        _serverManager = serverManager;
    }

    /// <inheritdoc />
    public async Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
        where TPacket : IPacket
    {
        var errors = new List<IResult>();

        foreach (var connectionHandler in _serverManager.ConnectionHandlers)
        {
            var config = _config.GetConfig(connectionHandler);
            if (config.SendRawPackets)
            {
                var result = await connectionHandler.SendMessageAsync
                    (new RawPacketMessage(packetArgs.Source, packetArgs.PacketString), ct);

                if (!result.IsSuccess)
                {
                    errors.Add(Result.FromError(result));
                }
            }

            if (config.SendDeserializedPackets)
            {
                var result = await connectionHandler.SendMessageAsync(new PacketMessage(packetArgs.Source, packetArgs.Packet), ct);

                if (!result.IsSuccess)
                {
                    errors.Add(Result.FromError(result));
                }
            }
        }

        return errors.Count switch
        {
            0 => Result.FromSuccess(),
            1 => (Result)errors[0],
            _ => new AggregateError(errors)
        };
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Local/Comms.cs => src/Local/NosSmooth.Comms.Local/Comms.cs +13 -0
@@ 0,0 1,13 @@
//
//  Comms.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.Comms.Core;
using NosSmooth.Core.Client;

namespace NosSmooth.Comms.Local;

public record Comms(Process NosTaleProcess, ConnectionHandler Connection, INostaleClient Client);
\ No newline at end of file

A src/Local/NosSmooth.Comms.Local/CommsInjector.cs => src/Local/NosSmooth.Comms.Local/CommsInjector.cs +154 -0
@@ 0,0 1,154 @@
//
//  CommsInjector.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.DependencyInjection;
using NosSmooth.Comms.Core;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.NamedPipes;
using NosSmooth.Injector;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Options;
using Remora.Results;

namespace NosSmooth.Comms.Local;

/// <summary>
/// Injects communication (tcp or named pipes) into a nostale process.
/// </summary>
public class CommsInjector
{
    private readonly IServiceProvider _serviceProvider;
    private readonly NosInjector _injector;
    private readonly NostaleClientResolver _resolver;

    /// <summary>
    /// Initializes a new instance of the <see cref="CommsInjector"/> class.
    /// </summary>
    /// <param name="serviceProvider">The service provider.</param>
    /// <param name="injector">The injector.</param>
    /// <param name="resolver">The nostale client resolver.</param>
    public CommsInjector(IServiceProvider serviceProvider, NosInjector injector, NostaleClientResolver resolver)
    {
        _serviceProvider = serviceProvider;
        _injector = injector;
        _resolver = resolver;
    }

    /// <summary>
    /// Find processes that are NosTale and create a <see cref="NosBrowserManager"/> from them.
    /// </summary>
    /// <param name="filterNames">The names to filter when searching the processes. In case the array is empty, look for all processes.</param>
    /// <returns>A list of the NosTale processes.</returns>
    public static IEnumerable<Result<NosBrowserManager>> CreateNostaleProcesssesBrowsers(params string[] filterNames)
    {
        return FindNosTaleProcesses(filterNames)
            .Select
            (
                x =>
                {
                    var manager = new NosBrowserManager
                    (
                        x,
                        new PlayerManagerOptions(),
                        new SceneManagerOptions(),
                        new PetManagerOptions(),
                        new NetworkManagerOptions(),
                        new UnitManagerOptions()
                    );

                    var initResult = manager.Initialize();
                    if (!initResult.IsSuccess)
                    {
                        return Result<NosBrowserManager>.FromError(initResult.Error);
                    }

                    return manager;
                }
            );
    }

    /// <summary>
    /// Find processes that are NosTale.
    /// </summary>
    /// <param name="filterNames">The names to filter when searching the processes. In case the array is empty, look for all processes.</param>
    /// <returns>A list of the NosTale processes.</returns>
    public static IEnumerable<Process> FindNosTaleProcesses(params string[] filterNames)
    {
        var processes = Process.GetProcesses().AsEnumerable();

        if (filterNames.Length > 0)
        {
            processes = processes.Where(x => filterNames.Contains(x.ProcessName));
        }

        return processes
            .Where
            (
                x =>
                {
                    try
                    {
                        return NosBrowserManager.IsProcessNostaleProcess(x);
                    }
                    catch
                    {
                        return false;
                    }
                }
            );
    }

    /// <summary>
    /// Inject NosSmooth.Comms.Inject.dll into the process,
    /// enable tcp server and establish a connection to the server.
    /// </summary>
    /// <returns>The result containing information about the established connection.</returns>
    public Task<Result<Comms>>
        EstablishTcpConnectionAsync()
    {
        throw new NotImplementedException();
    }

    /// <summary>
    /// Inject NosSmooth.Comms.Inject.dll into the process,
    /// enable named pipes server and establish a connection to the server.
    /// </summary>
    /// <param name="process">The process to establish named pipes with.</param>
    /// <param name="stopToken">The token used for stopping the connection.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>The result containing information about the established connection.</returns>
    public async Task<Result<Comms>> EstablishNamedPipesConnectionAsync
        (Process process, CancellationToken stopToken, CancellationToken ct)
    {
        var injectResult = _injector.Inject
        (
            process,
            Path.GetFullPath("NosSmooth.Comms.Inject.dll"),
            "NosSmooth.Comms.Inject.DllMain, NosSmooth.Comms.Inject",
            "EnableNamedPipes"
        );
        if (!injectResult.IsSuccess)
        {
            return Result<Comms>.FromError(injectResult);
        }

        var namedPipeClient = new NamedPipeClient($"NosSmooth_{process.Id}");

        var connectionResult = await namedPipeClient.ConnectAsync(ct);
        if (!connectionResult.IsSuccess)
        {
            return Result<Comms>.FromError(connectionResult);
        }

        var handler = ActivatorUtilities.CreateInstance<ConnectionHandler>
            (_serviceProvider, (IConnection)namedPipeClient);
        handler.StartHandler(stopToken);

        var nostaleClient = _resolver.Resolve(handler);
        return new Comms(process, handler, nostaleClient);
    }
}
\ No newline at end of file

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

using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Comms.Core.Extensions;
using NosSmooth.Comms.Local.MessageResponders;
using NosSmooth.Injector;

namespace NosSmooth.Comms.Local.Extensions;

/// <summary>
/// Extension methods for <see cref="IServiceCollection"/>.
/// </summary>
public static class ServiceCollectionExtensions
{
    /// <summary>
    /// Add <see cref="CommsInjector"/>.
    /// </summary>
    /// <param name="serviceCollection">The service ocllection.</param>
    /// <returns>The same service collection.</returns>
    public static IServiceCollection AddLocalComms(this IServiceCollection serviceCollection)
    {
        return serviceCollection
            .AddMultiClientHandling()
            .AddMessageResponder<PacketResponder>()
            .AddMessageResponder<RawPacketResponder>()
            .AddSingleton<NosInjector>()
            .AddSingleton<CommsInjector>();
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs => src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs +56 -0
@@ 0,0 1,56 @@
//
//  PacketResponder.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.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.Core.Client;
using NosSmooth.Core.Packets;
using NosSmooth.PacketSerializer;
using Remora.Results;

namespace NosSmooth.Comms.Local.MessageResponders;

/// <summary>
/// Responds to deserialized packets.
/// </summary>
public class PacketResponder : IMessageResponder<PacketMessage>
{
    private readonly INostaleClient _client;
    private readonly PacketHandler _packetHandler;
    private readonly IPacketSerializer _serializer;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketResponder"/> class.
    /// </summary>
    /// <param name="client">The nostale client.</param>
    /// <param name="packetHandler">The packet handler.</param>
    /// <param name="serializer">The serializer.</param>
    public PacketResponder(INostaleClient client, PacketHandler packetHandler, IPacketSerializer serializer)
    {
        _client = client;
        _packetHandler = packetHandler;
        _serializer = serializer;
    }

    /// <inheritdoc />
    public Task<Result> Respond(PacketMessage message, CancellationToken ct = default)
    {
        var serializedResult = _serializer.Serialize(message.Packet);
        if (!serializedResult.IsDefined(out var serialized))
        {
            return Task.FromResult(Result.FromError(serializedResult));
        }

        return _packetHandler.HandlePacketAsync
        (
            _client,
            message.Source,
            message.Packet,
            serialized,
            ct
        );
    }
}
\ No newline at end of file

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

using Microsoft.Extensions.Logging;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
using NosSmooth.Core.Client;
using NosSmooth.Core.Extensions;
using NosSmooth.Core.Packets;
using NosSmooth.Packets;
using NosSmooth.PacketSerializer;
using NosSmooth.PacketSerializer.Errors;
using Remora.Results;

namespace NosSmooth.Comms.Local.MessageResponders;

/// <summary>
/// Responds to raw packets.
/// </summary>
public class RawPacketResponder : IMessageResponder<RawPacketMessage>
{
    private readonly INostaleClient _client;
    private readonly PacketHandler _packetHandler;
    private readonly IPacketSerializer _serializer;
    private readonly ILogger<RawPacketResponder> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="RawPacketResponder"/> class.
    /// </summary>
    /// <param name="client">The nostale client.</param>
    /// <param name="packetHandler">The packet handler.</param>
    /// <param name="serializer">The serializer.</param>
    /// <param name="logger">The logger.</param>
    public RawPacketResponder(INostaleClient client, PacketHandler packetHandler, IPacketSerializer serializer, ILogger<RawPacketResponder> logger)
    {
        _client = client;
        _packetHandler = packetHandler;
        _serializer = serializer;
        _logger = logger;
    }

    /// <inheritdoc />
    public Task<Result> Respond(RawPacketMessage message, CancellationToken ct = default)
    {
        var deserializedResult = _serializer.Deserialize(message.Packet, message.Source);
        IPacket packet;

        if (!deserializedResult.IsSuccess)
        {
            if (deserializedResult.Error is not PacketConverterNotFoundError)
            {
                _logger.LogWarning("Could not parse {Packet}. Reason:", message.Packet);
                _logger.LogResultError(deserializedResult);
                packet = new ParsingFailedPacket(deserializedResult, message.Packet);
            }
            else
            {
                packet = new UnresolvedPacket(message.Packet.Split(' ')[0], message.Packet);
            }
        }
        else
        {
            packet = deserializedResult.Entity;
        }

        return _packetHandler.HandlePacketAsync
        (
            _client,
            message.Source,
            packet,
            message.Packet,
            ct
        );
    }
}
\ No newline at end of file

A src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj => src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj +28 -0
@@ 0,0 1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
      <Reference Include="NosSmooth.LocalBinding">
        <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.localbinding\1.0.0\lib\net7.0\NosSmooth.LocalBinding.dll</HintPath>
      </Reference>
      <Reference Include="Remora.Results">
        <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\remora.results\7.2.3\lib\net7.0\Remora.Results.dll</HintPath>
      </Reference>
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\..\Core\NosSmooth.Comms.Core\NosSmooth.Comms.Core.csproj" />
      <ProjectReference Include="..\..\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj" />
    </ItemGroup>

    <ItemGroup>
      <PackageReference Include="NosSmooth.Injector" Version="1.0.0" />
      <PackageReference Include="NosSmooth.LocalBinding" Version="1.0.0" />
    </ItemGroup>

</Project>

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

namespace NosSmooth.Comms.LocalData;

public record FocusMessage(long? EntityId);
\ No newline at end of file

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

namespace NosSmooth.Comms.LocalData;

public record FollowMessage(long? EntityId);
\ No newline at end of file

A src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj => src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj +9 -0
@@ 0,0 1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net7.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

</Project>

Do not follow this link