~ruther/NosSmooth.Comms

ae864d55d0a530321f0f9e9d9a2df6e6195e3bed — František Boháček 2 years ago dff1dfa
feat: add RunClientRequest message with support of setting binding options
M src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs => src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs +21 -7
@@ 133,11 133,14 @@ 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>
    /// <param name="message">The request message.</param>
    /// <param name="filter">The filter to check if the response is meant for the request.</param>
    /// <typeparam name="TMessage">The type of the message.</typeparam>
    /// <typeparam name="TResponse">The type of the response.</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)
    public IContract<TResponse, DefaultStates> ContractCustomResponse<TMessage, TResponse>(TMessage message, Func<TResponse, bool> filter)
        where TResponse : notnull
    {
        if (_contractor is null)
        {


@@ 147,13 150,13 @@ public class ConnectionHandler
            );
        }

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


@@ 163,9 166,9 @@ public class ConnectionHandler
                },
                DefaultStates.Requested
            )
            .SetMoveFilter<HandshakeResponse>
                (DefaultStates.Requested, DefaultStates.ResponseObtained)
            .SetFillData<HandshakeResponse>(DefaultStates.ResponseObtained, r => r)
            .SetMoveFilter<TResponse>
                (DefaultStates.Requested, filter, DefaultStates.ResponseObtained)
            .SetFillData<TResponse>(DefaultStates.ResponseObtained, r => r)
            .Build();
    }



@@ 173,6 176,17 @@ 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)
        => ContractCustomResponse<HandshakeRequest, HandshakeResponse>(handshake, _ => true);

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

M src/Local/NosSmooth.Comms.Inject/ClientState.cs => src/Local/NosSmooth.Comms.Inject/ClientState.cs +53 -0
@@ 4,6 4,9 @@
//  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.Inject.Messages;
using NosSmooth.LocalBinding;
using NosSmooth.LocalBinding.Options;
using Remora.Results;

namespace NosSmooth.Comms.Inject;


@@ 14,6 17,16 @@ namespace NosSmooth.Comms.Inject;
public class ClientState
{
    /// <summary>
    /// Gets the token cancelled upon starting the client (<see cref="RunClientRequest"/> received).
    /// </summary>
    public CancellationTokenSource Starting { get; } = new CancellationTokenSource();

    /// <summary>
    /// Gets the token cancelled when client has started (inside of <see cref="NosSmoothService"/>).
    /// </summary>
    public CancellationTokenSource Started { get; } = new CancellationTokenSource();

    /// <summary>
    /// Gets or sets whether the client is running.
    /// </summary>
    public bool IsRunning { get; internal set; }


@@ 22,4 35,44 @@ public class ClientState
    /// Gets or sets the result that was obtained upon initialization of the client.
    /// </summary>
    public Result? InitResult { get; internal set; }

    /// <summary>
    /// Gets or sets the result that was obtained from initialization of <see cref="NosBindingManager"/>.
    /// </summary>
    public Result? BindingResult { get; internal set; }

    /// <summary>
    /// Gets or sets hook manager options.
    /// </summary>
    public HookManagerOptions? HookOptions { get; internal set; }

    /// <summary>
    /// Gets or sets unit manager options.
    /// </summary>
    public UnitManagerOptions? UnitManagerOptions { get; internal set; }

    /// <summary>
    /// Gets or sets scene manager options.
    /// </summary>
    public SceneManagerOptions? SceneManagerOptions { get; internal set; }

    /// <summary>
    /// Gets or sets network manager options.
    /// </summary>
    public NetworkManagerOptions? NetworkManagerOptions { get; internal set; }

    /// <summary>
    /// Gets or sets player manager options.
    /// </summary>
    public PlayerManagerOptions? PlayerManagerOptions { get; internal set; }

    /// <summary>
    /// Gets or sets pet manager options.
    /// </summary>
    public PetManagerOptions? PetManagerOptions { get; internal set; }

    /// <summary>
    /// Gets or sets nt client options.
    /// </summary>
    public NtClientOptions? NtClientOptions { get; internal set; }
}
\ No newline at end of file

M src/Local/NosSmooth.Comms.Inject/DllMain.cs => src/Local/NosSmooth.Comms.Inject/DllMain.cs +13 -2
@@ 12,10 12,12 @@ using Microsoft.Extensions.Logging;
using NosSmooth.Comms.Core;
using NosSmooth.Comms.Core.Extensions;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Inject.Extensions;
using NosSmooth.Comms.Inject.MessageResponders;
using NosSmooth.Comms.Inject.PacketResponders;
using NosSmooth.Core.Extensions;
using NosSmooth.Extensions.SharedBinding.Extensions;
using NosSmooth.LocalBinding.Options;
using NosSmooth.LocalClient.Extensions;
using Remora.Results;



@@ 98,6 100,7 @@ public class DllMain
            return;
        }

        var clientState = new ClientState();
        _host = Host.CreateDefaultBuilder()
            .UseConsoleLifetime()
            .ConfigureLogging


@@ 114,7 117,7 @@ public class DllMain
                s =>
                {
                    s
                        .AddSingleton<ClientState>()
                        .AddSingleton<ClientState>(_ => clientState)
                        .AddSingleton<CallbackConfigRepository>()
                        .AddManagedNostaleCore()
                        .AddLocalClient()


@@ 127,7 130,15 @@ public class DllMain
                        .AddMessageResponder<FollowResponder>()
                        .AddMessageResponder<HandshakeResponder>()
                        .AddMessageResponder<ConsoleResponder>()
                        .AddMessageResponder<PacketResponder>();
                        .AddMessageResponder<PacketResponder>()
                        .AddMessageResponder<RunClientResponder>()
                        .Configure<HookManagerOptions>(clientState.HookOptions.CopyProperties)
                        .Configure<PlayerManagerOptions>(clientState.PlayerManagerOptions.CopyProperties)
                        .Configure<NetworkManagerOptions>(opts => clientState.NetworkManagerOptions.CopyProperties(opts))
                        .Configure<SceneManagerOptions>(clientState.SceneManagerOptions.CopyProperties)
                        .Configure<UnitManagerOptions>(clientState.UnitManagerOptions.CopyProperties)
                        .Configure<NtClientOptions>(clientState.NtClientOptions.CopyProperties)
                        .Configure<PetManagerOptions>(clientState.PetManagerOptions.CopyProperties);
                    s.AddHostedService<NosSmoothService>();
                }
            ).Build();

A src/Local/NosSmooth.Comms.Inject/Extensions/ObjectExtensions.cs => src/Local/NosSmooth.Comms.Inject/Extensions/ObjectExtensions.cs +39 -0
@@ 0,0 1,39 @@
//
//  ObjectExtensions.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.Reflection;

namespace NosSmooth.Comms.Inject.Extensions;

/// <summary>
/// Extension methods for object.
/// </summary>
public static class ObjectExtensions
{
    /// <summary>
    /// Extension for 'Object' that copies the properties to a destination object.
    /// </summary>
    /// <param name="source">The source.</param>
    /// <param name="destination">The destination.</param>
    /// <typeparam name="T">The type.</typeparam>
    public static void CopyProperties<T>(this T? source, T destination)
        where T : notnull
    {
        if (source is null)
        {
            return;
        }

        var properties = source.GetType().GetProperties();

        foreach (var p in properties.Where(prop => prop.CanRead && prop.CanWrite))
        {
            object? copyValue = p.GetValue(source);
            p.SetValue(destination, copyValue);
        }
    }
}
\ No newline at end of file

M src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs +6 -15
@@ 19,7 19,6 @@ namespace NosSmooth.Comms.Inject.MessageResponders;
public class HandshakeResponder : IMessageResponder<HandshakeRequest>
{
    private readonly ClientState _state;
    private readonly NosBrowserManager _browserManager;
    private readonly ConnectionHandler _connectionHandler;
    private readonly CallbackConfigRepository _config;
    private readonly ILogger<HandshakeResponder> _logger;


@@ 35,14 34,12 @@ public class HandshakeResponder : IMessageResponder<HandshakeRequest>
    public HandshakeResponder
    (
        ClientState state,
        NosBrowserManager browserManager,
        ConnectionHandler connectionHandler,
        CallbackConfigRepository config,
        ILogger<HandshakeResponder> logger
    )
    {
        _state = state;
        _browserManager = browserManager;
        _connectionHandler = connectionHandler;
        _config = config;
        _logger = logger;


@@ 54,20 51,14 @@ public class HandshakeResponder : IMessageResponder<HandshakeRequest>
        var config = new CallbackConfig(message.SendRawPackets, message.SendDeserializedPackets);
        _config.SetConfig(_connectionHandler, config);

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

        if (_browserManager.PlayerManager.TryGet(out var playerManager) &&
            _browserManager.IsInGame.TryGet(out var isInGame) &&
            isInGame)
        {
            playerName = playerManager.Player.Name;
            playerId = playerManager.PlayerId;
        }

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


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

namespace NosSmooth.Comms.Inject.MessageResponders;

/// <summary>
/// A responder to <see cref="RunClientRequest"/>.
/// </summary>
public class RunClientResponder : IMessageResponder<RunClientRequest>
{
    private readonly ClientState _state;
    private readonly ConnectionHandler _connectionHandler;

    /// <summary>
    /// Initializes a new instance of the <see cref="RunClientResponder"/> class.
    /// </summary>
    /// <param name="state">The client state.</param>
    /// <param name="connectionHandler">The connection handler wrapper.</param>
    public RunClientResponder
    (
        ClientState state,
        ConnectionHandler connectionHandler
    )
    {
        _state = state;
        _connectionHandler = connectionHandler;
    }

    /// <inheritdoc />
    public async Task<Result> Respond(RunClientRequest request, CancellationToken ct = default)
    {
        _state.HookOptions = request.HookOptions;
        _state.NetworkManagerOptions = request.NetworkManagerOptions;
        _state.NtClientOptions = request.NtClientOptions;
        _state.PetManagerOptions = request.PetManagerOptions;
        _state.PlayerManagerOptions = request.PlayerManagerOptions;
        _state.SceneManagerOptions = request.SceneManagerOptions;
        _state.UnitManagerOptions = request.UnitManagerOptions;
        _state.UnitManagerOptions = request.UnitManagerOptions;

        if (!_state.Starting.IsCancellationRequested)
        { // start the client.
            _state.Starting.Cancel();
        }

        if (!_state.Started.IsCancellationRequested)
        { // wait until the client has started.
            try
            {
                await Task.Delay(-1, _state.Started.Token);
            }
            catch
            {
                // ignored
            }
        }

        var sendMessageResult = await _connectionHandler.SendMessageAsync
            (new RunClientResponse(_state.InitResult, _state.BindingResult), ct);

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

A src/Local/NosSmooth.Comms.Inject/Messages/RunClientRequest.cs => src/Local/NosSmooth.Comms.Inject/Messages/RunClientRequest.cs +35 -0
@@ 0,0 1,35 @@
//
//  RunClientRequest.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.LocalBinding.Options;

namespace NosSmooth.Comms.Inject.Messages;

/// <summary>
/// Request to run NosTale client.
/// </summary>
/// <remarks>
/// Should be used if <see cref="HandshakeResponse"/>
/// contained not started .
/// </remarks>
/// <param name="HookOptions"></param>
/// <param name="UnitManagerOptions"></param>
/// <param name="SceneManagerOptions"></param>
/// <param name="NetworkManagerOptions"></param>
/// <param name="PlayerManagerOptions"></param>
/// <param name="PetManagerOptions"></param>
/// <param name="NtClientOptions"></param>
public record RunClientRequest
(
    HookManagerOptions? HookOptions = default,
    UnitManagerOptions? UnitManagerOptions = default,
    SceneManagerOptions? SceneManagerOptions = default,
    NetworkManagerOptions? NetworkManagerOptions = default,
    PlayerManagerOptions? PlayerManagerOptions = default,
    PetManagerOptions? PetManagerOptions = default,
    NtClientOptions? NtClientOptions = default
);
\ No newline at end of file

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

/// <summary>
/// A response to run client.
/// </summary>
/// <param name="InitializationResult">The run result. If error, that means the client is not running.</param>
/// <param name="BindingManagerResult">The result from binding manager initialization.</param>
public record RunClientResponse
(
    Result? InitializationResult,
    Result? BindingManagerResult
);
\ No newline at end of file

M src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs => src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs +41 -11
@@ 5,6 5,7 @@
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;


@@ 25,7 26,6 @@ public class NosSmoothService : BackgroundService
    private readonly IServiceProvider _services;
    private readonly ClientState _state;
    private readonly IPacketTypesRepository _packetTypesRepository;
    private readonly NosBindingManager _bindingManager;
    private readonly ILogger<NosSmoothService> _logger;

    /// <summary>


@@ 34,27 34,38 @@ public class NosSmoothService : BackgroundService
    /// <param name="services">The services.</param>
    /// <param name="state">The state of the application.</param>
    /// <param name="packetTypesRepository">The packet types repository.</param>
    /// <param name="bindingManager">The binding manager.</param>
    /// <param name="logger">The logger.</param>
    public NosSmoothService
    (
        IServiceProvider services,
        ClientState state,
        IPacketTypesRepository packetTypesRepository,
        NosBindingManager bindingManager,
        ILogger<NosSmoothService> logger
    )
    {
        _services = services;
        _state = state;
        _packetTypesRepository = packetTypesRepository;
        _bindingManager = bindingManager;
        _logger = logger;
    }

    /// <inheritdoc />
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var ctSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken, _state.Starting.Token);
        try
        {
            await Task.Delay(-1, ctSource.Token);
        }
        catch (Exception e)
        {
        }

        if (stoppingToken.IsCancellationRequested)
        {
            return;
        }

        var packetResult = _packetTypesRepository.AddDefaultPackets();
        if (!packetResult.IsSuccess)
        {


@@ 64,20 75,39 @@ public class NosSmoothService : BackgroundService
            _logger.LogResultError(packetResult);
        }

        var bindingResult = _bindingManager.Initialize();
        var bindingResult = _services.GetRequiredService<NosBindingManager>().Initialize();
        if (!bindingResult.IsSuccess)
        {
            _state.InitResult = Result.FromError(bindingResult.Error);
            _logger.LogError("Could not initialize the binding manager");
            _logger.LogError("Could not initialize the binding manager.");
            _logger.LogResultError(bindingResult);
            await Task.Delay(-1, stoppingToken);
            return;
            _state.BindingResult = Result.FromError
                (new GenericError("Binding manager not initialized correctly."), bindingResult);
        }
        else
        {
            _state.BindingResult = Result.FromSuccess();
        }

        _state.IsRunning = true;
        var nostaleClient = _services.GetRequiredService<INostaleClient>();
        var runResult = await nostaleClient.RunAsync(stoppingToken);
        _state.IsRunning = false;
        var runTask = nostaleClient.RunAsync(stoppingToken);
        await Task.Delay(10, stoppingToken);
        Result runResult;
        if (runTask.IsCompleted)
        {
            runResult = await runTask;
            _state.InitResult = runResult;
            _state.Started.Cancel();
        }
        else
        {
            _state.IsRunning = true;
            _state.InitResult = Result.FromSuccess();
            _state.Started.Cancel();

            runResult = await runTask;
        }

        if (!runResult.IsSuccess)
        {
            _state.InitResult = runResult;

A src/Local/NosSmooth.Comms.Local/Extensions/ConnectionHandlerExtensions.cs => src/Local/NosSmooth.Comms.Local/Extensions/ConnectionHandlerExtensions.cs +27 -0
@@ 0,0 1,27 @@
//
//  ConnectionHandlerExtensions.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.Inject.Messages;
using NosSmooth.Core.Contracts;

namespace NosSmooth.Comms.Local.Extensions;

/// <summary>
/// Extension methods for <see cref="ConnectionHandler"/>.
/// </summary>
public static class ConnectionHandlerExtensions
{
    /// <summary>
    /// Contract <see cref="RunClientRequest"/>, <see cref="RunClientResponse"/>.
    /// </summary>
    /// <param name="connectionHandler">The connection handler.</param>
    /// <param name="message">The request.</param>
    /// <returns>The contract.</returns>
    public static IContract<RunClientResponse, DefaultStates> ContractRunClient
        (this ConnectionHandler connectionHandler, RunClientRequest message)
        => connectionHandler.ContractCustomResponse<RunClientRequest, RunClientResponse>(message, _ => true);
}
\ No newline at end of file

M src/Samples/ConsolePacketLogger/ClientService.cs => src/Samples/ConsolePacketLogger/ClientService.cs +40 -9
@@ 10,9 10,13 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Comms.Core;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Inject.Messages;
using NosSmooth.Comms.Local;
using NosSmooth.Comms.Local.Extensions;
using NosSmooth.Core.Contracts;
using NosSmooth.Core.Extensions;
using NosSmooth.LocalBinding.Options;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using NosSmooth.PacketSerializer.Extensions;
using NosSmooth.PacketSerializer.Packets;



@@ 84,20 88,47 @@ public class ClientService : BackgroundService

        if (!handshakeResponse.ClientRunning)
        {
            _logger.LogError("The client is not running?");
            _logger.LogInformation("The client is not running, going to start it");
        }
        else
        {
            if (handshakeResponse.InitializationResult is not null
                && !handshakeResponse.InitializationResult.Value.IsSuccess)
            {
                _logger.LogError("Received an error from the Inject assembly that failed on initialization.");
                _logger.LogResultError(handshakeResponse.InitializationResult);
            }
        }

        // should be run only if !handshakeResponse.ClientRunning
        // but will work correctly even if client is already running.
        var clientRunResult = await connection.Connection.ContractRunClient(new RunClientRequest())
            .WaitForAsync(DefaultStates.ResponseObtained, ct: stoppingToken);

        if (!clientRunResult.IsDefined(out var clientRun))
        {
            _logger.LogResultError(clientRunResult);
            _lifetime.StopApplication();
            return;
        }

        if (clientRun.InitializationResult is null)
        {
            _logger.LogError("Huh, the client did not return a result?");
        }

        if (!(clientRun.BindingManagerResult?.IsSuccess ?? true))
        {
            _logger.LogError("Binding manager threw an error.");
            _logger.LogResultError(clientRun.BindingManagerResult);
        }

        if (handshakeResponse.InitializationErrorfulResult is not null
            && !handshakeResponse.InitializationErrorfulResult.Value.IsSuccess)
        if (!(clientRun.InitializationResult?.IsSuccess ?? true))
        {
            _logger.LogError("Received an error from the Inject assembly that failed on initialization.");
            _logger.LogResultError(handshakeResponse.InitializationErrorfulResult);
            _logger.LogResultError(clientRun.InitializationResult);
        }

        _logger.LogInformation
        (
            $"Connected to {handshakeResponse.CharacterName ?? "Not in game"} ({handshakeResponse.CharacterId?.ToString() ?? "Not in game"})"
        );
        _logger.LogInformation($"Connected to NosTale");

        var runResult = await connection.Connection.RunHandlerAsync(stoppingToken);
        if (!runResult.IsSuccess)

Do not follow this link