From ae864d55d0a530321f0f9e9d9a2df6e6195e3bed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Wed, 15 Feb 2023 08:15:08 +0100 Subject: [PATCH] feat: add RunClientRequest message with support of setting binding options --- .../NosSmooth.Comms.Core/ConnectionHandler.cs | 28 ++++++-- .../NosSmooth.Comms.Inject/ClientState.cs | 53 ++++++++++++++ src/Local/NosSmooth.Comms.Inject/DllMain.cs | 15 +++- .../Extensions/ObjectExtensions.cs | 39 ++++++++++ .../MessageResponders/HandshakeResponder.cs | 21 ++---- .../MessageResponders/RunClientResponder.cs | 71 +++++++++++++++++++ .../Messages/RunClientRequest.cs | 35 +++++++++ .../Messages/RunClientResponse.cs | 20 ++++++ .../NosSmoothService.cs | 52 +++++++++++--- .../Extensions/ConnectionHandlerExtensions.cs | 27 +++++++ .../ConsolePacketLogger/ClientService.cs | 49 ++++++++++--- 11 files changed, 366 insertions(+), 44 deletions(-) create mode 100644 src/Local/NosSmooth.Comms.Inject/Extensions/ObjectExtensions.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/MessageResponders/RunClientResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/Messages/RunClientRequest.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/Messages/RunClientResponse.cs create mode 100644 src/Local/NosSmooth.Comms.Local/Extensions/ConnectionHandlerExtensions.cs diff --git a/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs b/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs index 425d1a7..50cc36b 100644 --- a/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs +++ b/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs @@ -133,11 +133,14 @@ public class ConnectionHandler /// Create a contract for sending a message, /// will be returned back. /// - /// The handshake request. + /// The request message. + /// The filter to check if the response is meant for the request. /// The type of the message. + /// The type of the response. /// A contract representing send message operation. /// Thrown in case contract is created on the server. Clients do not send responses. - public IContract ContractHanshake(HandshakeRequest handshake) + public IContract ContractCustomResponse(TMessage message, Func filter) + where TResponse : notnull { if (_contractor is null) { @@ -147,13 +150,13 @@ public class ConnectionHandler ); } - return new ContractBuilder(_contractor, DefaultStates.None) + return new ContractBuilder(_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.FromError(result); @@ -163,12 +166,23 @@ public class ConnectionHandler }, DefaultStates.Requested ) - .SetMoveFilter - (DefaultStates.Requested, DefaultStates.ResponseObtained) - .SetFillData(DefaultStates.ResponseObtained, r => r) + .SetMoveFilter + (DefaultStates.Requested, filter, DefaultStates.ResponseObtained) + .SetFillData(DefaultStates.ResponseObtained, r => r) .Build(); } + /// + /// Create a contract for sending a message, + /// will be returned back. + /// + /// The handshake request. + /// The type of the message. + /// A contract representing send message operation. + /// Thrown in case contract is created on the server. Clients do not send responses. + public IContract ContractHanshake(HandshakeRequest handshake) + => ContractCustomResponse(handshake, _ => true); + /// /// Create a contract for sending a message, /// will be returned back. diff --git a/src/Local/NosSmooth.Comms.Inject/ClientState.cs b/src/Local/NosSmooth.Comms.Inject/ClientState.cs index ab9836b..796dec6 100644 --- a/src/Local/NosSmooth.Comms.Inject/ClientState.cs +++ b/src/Local/NosSmooth.Comms.Inject/ClientState.cs @@ -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; @@ -13,6 +16,16 @@ namespace NosSmooth.Comms.Inject; /// public class ClientState { + /// + /// Gets the token cancelled upon starting the client ( received). + /// + public CancellationTokenSource Starting { get; } = new CancellationTokenSource(); + + /// + /// Gets the token cancelled when client has started (inside of ). + /// + public CancellationTokenSource Started { get; } = new CancellationTokenSource(); + /// /// Gets or sets whether the client is running. /// @@ -22,4 +35,44 @@ public class ClientState /// Gets or sets the result that was obtained upon initialization of the client. /// public Result? InitResult { get; internal set; } + + /// + /// Gets or sets the result that was obtained from initialization of . + /// + public Result? BindingResult { get; internal set; } + + /// + /// Gets or sets hook manager options. + /// + public HookManagerOptions? HookOptions { get; internal set; } + + /// + /// Gets or sets unit manager options. + /// + public UnitManagerOptions? UnitManagerOptions { get; internal set; } + + /// + /// Gets or sets scene manager options. + /// + public SceneManagerOptions? SceneManagerOptions { get; internal set; } + + /// + /// Gets or sets network manager options. + /// + public NetworkManagerOptions? NetworkManagerOptions { get; internal set; } + + /// + /// Gets or sets player manager options. + /// + public PlayerManagerOptions? PlayerManagerOptions { get; internal set; } + + /// + /// Gets or sets pet manager options. + /// + public PetManagerOptions? PetManagerOptions { get; internal set; } + + /// + /// Gets or sets nt client options. + /// + public NtClientOptions? NtClientOptions { get; internal set; } } \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Inject/DllMain.cs b/src/Local/NosSmooth.Comms.Inject/DllMain.cs index c6b2620..f7f18fe 100644 --- a/src/Local/NosSmooth.Comms.Inject/DllMain.cs +++ b/src/Local/NosSmooth.Comms.Inject/DllMain.cs @@ -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() + .AddSingleton(_ => clientState) .AddSingleton() .AddManagedNostaleCore() .AddLocalClient() @@ -127,7 +130,15 @@ public class DllMain .AddMessageResponder() .AddMessageResponder() .AddMessageResponder() - .AddMessageResponder(); + .AddMessageResponder() + .AddMessageResponder() + .Configure(clientState.HookOptions.CopyProperties) + .Configure(clientState.PlayerManagerOptions.CopyProperties) + .Configure(opts => clientState.NetworkManagerOptions.CopyProperties(opts)) + .Configure(clientState.SceneManagerOptions.CopyProperties) + .Configure(clientState.UnitManagerOptions.CopyProperties) + .Configure(clientState.NtClientOptions.CopyProperties) + .Configure(clientState.PetManagerOptions.CopyProperties); s.AddHostedService(); } ).Build(); diff --git a/src/Local/NosSmooth.Comms.Inject/Extensions/ObjectExtensions.cs b/src/Local/NosSmooth.Comms.Inject/Extensions/ObjectExtensions.cs new file mode 100644 index 0000000..d317d8a --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/Extensions/ObjectExtensions.cs @@ -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; + +/// +/// Extension methods for object. +/// +public static class ObjectExtensions +{ + /// + /// Extension for 'Object' that copies the properties to a destination object. + /// + /// The source. + /// The destination. + /// The type. + public static void CopyProperties(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 diff --git a/src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs b/src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs index dc4f075..f30a9fb 100644 --- a/src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs +++ b/src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs @@ -19,7 +19,6 @@ namespace NosSmooth.Comms.Inject.MessageResponders; public class HandshakeResponder : IMessageResponder { private readonly ClientState _state; - private readonly NosBrowserManager _browserManager; private readonly ConnectionHandler _connectionHandler; private readonly CallbackConfigRepository _config; private readonly ILogger _logger; @@ -35,14 +34,12 @@ public class HandshakeResponder : IMessageResponder public HandshakeResponder ( ClientState state, - NosBrowserManager browserManager, ConnectionHandler connectionHandler, CallbackConfigRepository config, ILogger logger ) { _state = state; - _browserManager = browserManager; _connectionHandler = connectionHandler; _config = config; _logger = logger; @@ -54,20 +51,14 @@ public class HandshakeResponder : IMessageResponder 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 ); diff --git a/src/Local/NosSmooth.Comms.Inject/MessageResponders/RunClientResponder.cs b/src/Local/NosSmooth.Comms.Inject/MessageResponders/RunClientResponder.cs new file mode 100644 index 0000000..960075d --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/MessageResponders/RunClientResponder.cs @@ -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; + +/// +/// A responder to . +/// +public class RunClientResponder : IMessageResponder +{ + private readonly ClientState _state; + private readonly ConnectionHandler _connectionHandler; + + /// + /// Initializes a new instance of the class. + /// + /// The client state. + /// The connection handler wrapper. + public RunClientResponder + ( + ClientState state, + ConnectionHandler connectionHandler + ) + { + _state = state; + _connectionHandler = connectionHandler; + } + + /// + public async Task 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 diff --git a/src/Local/NosSmooth.Comms.Inject/Messages/RunClientRequest.cs b/src/Local/NosSmooth.Comms.Inject/Messages/RunClientRequest.cs new file mode 100644 index 0000000..f52f24a --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/Messages/RunClientRequest.cs @@ -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; + +/// +/// Request to run NosTale client. +/// +/// +/// Should be used if +/// contained not started . +/// +/// +/// +/// +/// +/// +/// +/// +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 diff --git a/src/Local/NosSmooth.Comms.Inject/Messages/RunClientResponse.cs b/src/Local/NosSmooth.Comms.Inject/Messages/RunClientResponse.cs new file mode 100644 index 0000000..89da1c6 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/Messages/RunClientResponse.cs @@ -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; + +/// +/// A response to run client. +/// +/// The run result. If error, that means the client is not running. +/// The result from binding manager initialization. +public record RunClientResponse +( + Result? InitializationResult, + Result? BindingManagerResult +); \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs b/src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs index 2c4b4b7..d1f3e0e 100644 --- a/src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs +++ b/src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs @@ -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 _logger; /// @@ -34,27 +34,38 @@ public class NosSmoothService : BackgroundService /// The services. /// The state of the application. /// The packet types repository. - /// The binding manager. /// The logger. public NosSmoothService ( IServiceProvider services, ClientState state, IPacketTypesRepository packetTypesRepository, - NosBindingManager bindingManager, ILogger logger ) { _services = services; _state = state; _packetTypesRepository = packetTypesRepository; - _bindingManager = bindingManager; _logger = logger; } /// 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().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(); - 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; diff --git a/src/Local/NosSmooth.Comms.Local/Extensions/ConnectionHandlerExtensions.cs b/src/Local/NosSmooth.Comms.Local/Extensions/ConnectionHandlerExtensions.cs new file mode 100644 index 0000000..3a46559 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Local/Extensions/ConnectionHandlerExtensions.cs @@ -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; + +/// +/// Extension methods for . +/// +public static class ConnectionHandlerExtensions +{ + /// + /// Contract , . + /// + /// The connection handler. + /// The request. + /// The contract. + public static IContract ContractRunClient + (this ConnectionHandler connectionHandler, RunClientRequest message) + => connectionHandler.ContractCustomResponse(message, _ => true); +} \ No newline at end of file diff --git a/src/Samples/ConsolePacketLogger/ClientService.cs b/src/Samples/ConsolePacketLogger/ClientService.cs index 8f54bbf..bad9011 100644 --- a/src/Samples/ConsolePacketLogger/ClientService.cs +++ b/src/Samples/ConsolePacketLogger/ClientService.cs @@ -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) -- 2.48.1