From ecfc8cd7dc9143e9550044f3f5971bf8631f51b7 Mon Sep 17 00:00:00 2001 From: Rutherther Date: Tue, 31 Jan 2023 12:57:26 +0100 Subject: [PATCH] feat: add local injectable and injector to allow making connections to nostale processes --- NosSmooth.Comms.sln | 4 +- .../Messages/HandshakeRequest.cs | 2 +- .../NosSmooth.Comms.Abstractions.csproj | 1 + .../ClientNostaleClient.cs | 37 +++-- .../NosSmooth.Comms.Core/ConnectionHandler.cs | 57 ++++++- .../Errors/MessageHandlerNotFoundError.cs | 14 ++ .../NosSmooth.Comms.Core/MessageHandler.cs | 27 ++- .../NosSmooth.Comms.Core.csproj | 1 + .../NosSmooth.Comms.Core/ServerManager.cs | 69 +++++++- .../NamedPipeServer.cs | 46 ++++-- .../NosSmooth.Comms.Inject/CallbackConfig.cs | 9 + .../CallbackConfigRepository.cs | 46 ++++++ src/Local/NosSmooth.Comms.Inject/DllMain.cs | 129 +++++++++++++++ .../MessageResponders/CommandResponder.cs | 34 ++++ .../MessageResponders/FocusResponder.cs | 63 +++++++ .../MessageResponders/FollowResponder.cs | 70 ++++++++ .../MessageResponders/HandshakeResponder.cs | 77 +++++++++ .../MessageResponders/PacketResponder.cs | 52 ++++++ .../NosSmooth.Comms.Inject.csproj | 26 +++ .../NosSmoothService.cs | 79 +++++++++ .../PacketResponders/EveryPacketResponder.cs | 70 ++++++++ src/Local/NosSmooth.Comms.Local/Comms.cs | 13 ++ .../NosSmooth.Comms.Local/CommsInjector.cs | 154 ++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 33 ++++ .../MessageResponders/PacketResponder.cs | 56 +++++++ .../MessageResponders/RawPacketResponder.cs | 78 +++++++++ .../NosSmooth.Comms.Local.csproj | 28 ++++ .../NosSmooth.Comms.LocalData/FocusMessage.cs | 9 + .../FollowMessage.cs | 9 + .../NosSmooth.Comms.LocalData.csproj | 9 + 30 files changed, 1263 insertions(+), 39 deletions(-) create mode 100644 src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/CallbackConfig.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/DllMain.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj create mode 100644 src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs create mode 100644 src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Local/Comms.cs create mode 100644 src/Local/NosSmooth.Comms.Local/CommsInjector.cs create mode 100644 src/Local/NosSmooth.Comms.Local/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Local/MessageResponders/RawPacketResponder.cs create mode 100644 src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj create mode 100644 src/Local/NosSmooth.Comms.LocalData/FocusMessage.cs create mode 100644 src/Local/NosSmooth.Comms.LocalData/FollowMessage.cs create mode 100644 src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj diff --git a/NosSmooth.Comms.sln b/NosSmooth.Comms.sln index 4083b34..cd42937 100644 --- a/NosSmooth.Comms.sln +++ b/NosSmooth.Comms.sln @@ -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 diff --git a/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs b/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs index 6c896a2..86b403b 100644 --- a/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs +++ b/src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs @@ -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 diff --git a/src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj b/src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj index ce5dd66..ab691d0 100644 --- a/src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj +++ b/src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs b/src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs index 69ab2bf..21f0d7b 100644 --- a/src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs +++ b/src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs @@ -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 /// public async Task 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); } /// public async Task 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); } /// public async Task 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); } /// public async Task 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); } /// public async Task 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 diff --git a/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs b/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs index c8678ef..425d1a7 100644 --- a/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs +++ b/src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs @@ -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,8 +52,19 @@ public class ConnectionHandler _messageHandler = messageHandler; _options = options.Value; _logger = logger; + Id = Guid.NewGuid(); } + /// + /// Gets the id of the connection. + /// + public Guid Id { get; } + + /// + /// The connection has been closed. + /// + public event EventHandler? Closed; + /// /// Gets the connection. /// @@ -86,19 +98,19 @@ public class ConnectionHandler private async Task 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,9 +125,50 @@ public class ConnectionHandler } _connection.Disconnect(); + Closed?.Invoke(this, EventArgs.Empty); return Result.FromSuccess(); } + /// + /// 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) + { + 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(_contractor, DefaultStates.None) + .SetMoveAction + ( + DefaultStates.None, + async (a, ct) => + { + var result = await SendMessageAsync(handshake, ct); + if (!result.IsDefined(out _)) + { + return Result.FromError(result); + } + + return true; + }, + DefaultStates.Requested + ) + .SetMoveFilter + (DefaultStates.Requested, DefaultStates.ResponseObtained) + .SetFillData(DefaultStates.ResponseObtained, r => r) + .Build(); + } + /// /// Create a contract for sending a message, /// will be returned back. diff --git a/src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs b/src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs new file mode 100644 index 0000000..6ecd487 --- /dev/null +++ b/src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs @@ -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; + +/// +/// No message handler was found for the received message. +/// +public record MessageHandlerNotFoundError() : ResultError("Message handler for the given message was not found."); \ No newline at end of file diff --git a/src/Core/NosSmooth.Comms.Core/MessageHandler.cs b/src/Core/NosSmooth.Comms.Core/MessageHandler.cs index e12007f..013c935 100644 --- a/src/Core/NosSmooth.Comms.Core/MessageHandler.cs +++ b/src/Core/NosSmooth.Comms.Core/MessageHandler.cs @@ -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 GenericHandleMessageAsync (ConnectionHandler connection, MessageWrapper wrappedMessage, CancellationToken ct) + where TMessage : notnull { var data = wrappedMessage.Data; + var contractor = _services.GetService(); + 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(); injector.ConnectionHandler = connection; @@ -77,7 +91,8 @@ public class MessageHandler var responders = scope.ServiceProvider .GetServices>() - .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(), diff --git a/src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj b/src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj index 4efb34d..ea5bca4 100644 --- a/src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj +++ b/src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj @@ -22,6 +22,7 @@ + diff --git a/src/Core/NosSmooth.Comms.Core/ServerManager.cs b/src/Core/NosSmooth.Comms.Core/ServerManager.cs index ae125b1..b319bbd 100644 --- a/src/Core/NosSmooth.Comms.Core/ServerManager.cs +++ b/src/Core/NosSmooth.Comms.Core/ServerManager.cs @@ -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 _logger; private readonly ILogger _handlerLogger; private readonly List _connectionHandlers; + private readonly ReaderWriterLockSlim _readerWriterLock; private Task? _task; private CancellationTokenSource? _ctSource; @@ -46,12 +46,32 @@ public class ServerManager { _server = server; _connectionHandlers = new List(); + _readerWriterLock = new ReaderWriterLockSlim(); _messageHandler = messageHandler; _options = options; _logger = logger; _handlerLogger = handlerLogger; } + /// + /// Gets connection handlers. + /// + public IReadOnlyList ConnectionHandlers + { + get + { + _readerWriterLock.EnterReadLock(); + try + { + return _connectionHandlers.ToArray(); + } + finally + { + _readerWriterLock.ExitReadLock(); + } + } + } + /// /// Run the manager and await the task. /// @@ -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 errors = new List(); - 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 { diff --git a/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs b/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs index ec07069..62940b6 100644 --- a/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs +++ b/src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs @@ -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(_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(_connections); - _readerWriterLock.ExitReadLock(); + IReadOnlyList connections; + try + { + connections = new List(_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 diff --git a/src/Local/NosSmooth.Comms.Inject/CallbackConfig.cs b/src/Local/NosSmooth.Comms.Inject/CallbackConfig.cs new file mode 100644 index 0000000..15be3bf --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/CallbackConfig.cs @@ -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 diff --git a/src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs b/src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs new file mode 100644 index 0000000..bf10f25 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs @@ -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; + +/// +/// A repository containing configurations for given connection handlers. +/// +public class CallbackConfigRepository +{ + private readonly ConcurrentDictionary _configs; + + /// + /// Initializes a new instance of the class. + /// + public CallbackConfigRepository() + { + _configs = new ConcurrentDictionary(); + } + + /// + /// Get config of the given connection, or default. + /// + /// The connection to get config of. + /// A config for the connection. + public CallbackConfig GetConfig(ConnectionHandler connection) + { + return _configs.GetValueOrDefault(connection, new CallbackConfig(false, false)); + } + + /// + /// Set config of the given connection. + /// + /// The connection to set config. + /// The config to set. + public void SetConfig(ConnectionHandler connection, CallbackConfig config) + { + _configs.AddOrUpdate(connection, _ => config, (a, b) => config); + } +} \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Inject/DllMain.cs b/src/Local/NosSmooth.Comms.Inject/DllMain.cs new file mode 100644 index 0000000..4ab54a9 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/DllMain.cs @@ -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; + +/// +/// A main entrypoint to NosSmooth local communications. +/// +public class DllMain +{ + private static IHost? _host; + + /// + /// Allocate console. + /// + /// Whether the operation was successful. + [DllImport("kernel32")] + public static extern bool AllocConsole(); + + /// + /// Enable named pipes server. + /// + [UnmanagedCallersOnly(EntryPoint = "EnableNamedPipes")] + public static void EnableNamedPipes() + { + Main + ( + host => + { + var manager = host.Services.GetRequiredService(); + return manager.RunManagerAsync(host.Services.GetRequiredService().ApplicationStopping); + } + ); + } + + private static void Main(Func> host) + { + AllocConsole(); + new Thread + ( + () => + { + try + { + MainEntry(host).GetAwaiter().GetResult(); + } + catch (Exception e) + { + Console.WriteLine(e.ToString()); + } + } + ).Start(); + } + + /// + /// The entrypoint method. + /// + /// A representing the asynchronous operation. + private static async Task MainEntry(Func> host) + { + if (_host is not null) + { + var result = await host(_host); + if (!result.IsSuccess) + { + _host.Services.GetRequiredService>().LogResultError(result); + } + return; + } + + _host = Host.CreateDefaultBuilder() + .UseConsoleLifetime() + .ConfigureLogging + ( + b => + { + b + .ClearProviders() + .AddConsole(); + } + ) + .ConfigureServices + ( + s => + { + s + .AddSingleton() + .AddNostaleCore() + .AddLocalClient() + .ShareNosSmooth() + .AddNamedPipeServer(p => $"NosSmooth_{Process.GetCurrentProcess().Id}") + .AddPacketResponder() + .AddServerHandling() + .AddMessageResponder() + .AddMessageResponder() + .AddMessageResponder() + .AddMessageResponder() + .AddMessageResponder(); + s.AddHostedService(); + } + ).Build(); + + await _host.StartAsync(); + var hostTask = _host.RunAsync(); + var serverTask = host(_host); + + await Task.WhenAll(hostTask, serverTask); + } +} \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs b/src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs new file mode 100644 index 0000000..ba5904e --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs @@ -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; + +/// +/// A responder to . +/// +public class CommandResponder : IMessageResponder +{ + private readonly INostaleClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The client. + public CommandResponder(INostaleClient client) + { + _client = client; + + } + + /// + public Task Respond(CommandMessage message, CancellationToken ct = default) + => _client.SendCommandAsync(message.Command, ct); +} \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs b/src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs new file mode 100644 index 0000000..b8e2ce1 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs @@ -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; + +/// +/// A resopnder to . +/// +public class FocusResponder : IMessageResponder +{ + private readonly NosBrowserManager _browserManager; + private readonly NosThreadSynchronizer _synchronizer; + private readonly IEntityFocusHook _focusHook; + + /// + /// Initializes a new instance of the class. + /// + /// The browser manager. + /// The synchronizer. + /// The focus hook. + public FocusResponder + (NosBrowserManager browserManager, NosThreadSynchronizer synchronizer, IEntityFocusHook focusHook) + { + _browserManager = browserManager; + _synchronizer = synchronizer; + _focusHook = focusHook; + } + + /// + public async Task 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 diff --git a/src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs b/src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs new file mode 100644 index 0000000..34c9e3f --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs @@ -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; + +/// +/// A responder to . +/// +public class FollowResponder : IMessageResponder +{ + private readonly NosBrowserManager _browserManager; + private readonly NosThreadSynchronizer _synchronizer; + private readonly IHookManager _hookManager; + + /// + /// Initializes a new instance of the class. + /// + /// The browser manager. + /// The synchronizer. + /// The hook manager. + public FollowResponder + (NosBrowserManager browserManager, NosThreadSynchronizer synchronizer, IHookManager hookManager) + { + _browserManager = browserManager; + _synchronizer = synchronizer; + _hookManager = hookManager; + } + + /// + public async Task 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 diff --git a/src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs b/src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs new file mode 100644 index 0000000..d34524a --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs @@ -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; + +/// +/// A responder to . +/// +public class HandshakeResponder : IMessageResponder +{ + private readonly NosBrowserManager _browserManager; + private readonly ConnectionHandler _connectionHandler; + private readonly CallbackConfigRepository _config; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The browser manager. + /// The connection handler. + /// The config. + /// The logger. + public HandshakeResponder + ( + NosBrowserManager browserManager, + ConnectionHandler connectionHandler, + CallbackConfigRepository config, + ILogger logger + ) + { + _browserManager = browserManager; + _connectionHandler = connectionHandler; + _config = config; + _logger = logger; + } + + /// + public async Task 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 diff --git a/src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs b/src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs new file mode 100644 index 0000000..b85e07b --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs @@ -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; + +/// +/// A responder to and . +/// +public class PacketResponder : IMessageResponder, IMessageResponder +{ + private readonly INostaleClient _client; + + /// + /// Initializes a new instance of the class. + /// + /// The NosTale client. + public PacketResponder(INostaleClient client) + { + _client = client; + } + + /// + public Task Respond(RawPacketMessage message, CancellationToken ct = default) + { + if (message.Source == PacketSource.Client) + { + return _client.SendPacketAsync(message.Packet, ct); + } + + return _client.ReceivePacketAsync(message.Packet, ct); + } + + /// + public Task 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 diff --git a/src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj b/src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj new file mode 100644 index 0000000..ea43598 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj @@ -0,0 +1,26 @@ + + + + net7.0 + enable + enable + true + true + + + + + + + + + + + + + + + + + + diff --git a/src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs b/src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs new file mode 100644 index 0000000..dd04dda --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs @@ -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; + +/// +/// Nostale client runner. +/// +public class NosSmoothService : BackgroundService +{ + private readonly IServiceProvider _services; + private readonly IPacketTypesRepository _packetTypesRepository; + private readonly NosBindingManager _bindingManager; + private readonly IHostLifetime _lifetime; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The services. + /// The packet types repository. + /// The binding manager. + /// The lifetime. + /// The logger. + public NosSmoothService + ( + IServiceProvider services, + IPacketTypesRepository packetTypesRepository, + NosBindingManager bindingManager, + IHostLifetime lifetime, + ILogger logger + ) + { + _services = services; + _packetTypesRepository = packetTypesRepository; + _bindingManager = bindingManager; + _lifetime = lifetime; + _logger = logger; + } + + /// + 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(); + var runResult = await nostaleClient.RunAsync(stoppingToken); + if (!runResult.IsSuccess) + { + _logger.LogResultError(runResult); + await _lifetime.StopAsync(default); + } + } +} \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs b/src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs new file mode 100644 index 0000000..9f50b88 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs @@ -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; + +/// +public class EveryPacketResponder : IEveryPacketResponder +{ + private readonly CallbackConfigRepository _config; + private readonly ServerManager _serverManager; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The server manager. + public EveryPacketResponder(CallbackConfigRepository config, ServerManager serverManager) + { + _config = config; + _serverManager = serverManager; + } + + /// + public async Task Respond(PacketEventArgs packetArgs, CancellationToken ct = default) + where TPacket : IPacket + { + var errors = new List(); + + 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 diff --git a/src/Local/NosSmooth.Comms.Local/Comms.cs b/src/Local/NosSmooth.Comms.Local/Comms.cs new file mode 100644 index 0000000..2819325 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Local/Comms.cs @@ -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 diff --git a/src/Local/NosSmooth.Comms.Local/CommsInjector.cs b/src/Local/NosSmooth.Comms.Local/CommsInjector.cs new file mode 100644 index 0000000..8b1eefa --- /dev/null +++ b/src/Local/NosSmooth.Comms.Local/CommsInjector.cs @@ -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; + +/// +/// Injects communication (tcp or named pipes) into a nostale process. +/// +public class CommsInjector +{ + private readonly IServiceProvider _serviceProvider; + private readonly NosInjector _injector; + private readonly NostaleClientResolver _resolver; + + /// + /// Initializes a new instance of the class. + /// + /// The service provider. + /// The injector. + /// The nostale client resolver. + public CommsInjector(IServiceProvider serviceProvider, NosInjector injector, NostaleClientResolver resolver) + { + _serviceProvider = serviceProvider; + _injector = injector; + _resolver = resolver; + } + + /// + /// Find processes that are NosTale and create a from them. + /// + /// The names to filter when searching the processes. In case the array is empty, look for all processes. + /// A list of the NosTale processes. + public static IEnumerable> 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.FromError(initResult.Error); + } + + return manager; + } + ); + } + + /// + /// Find processes that are NosTale. + /// + /// The names to filter when searching the processes. In case the array is empty, look for all processes. + /// A list of the NosTale processes. + public static IEnumerable 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; + } + } + ); + } + + /// + /// Inject NosSmooth.Comms.Inject.dll into the process, + /// enable tcp server and establish a connection to the server. + /// + /// The result containing information about the established connection. + public Task> + EstablishTcpConnectionAsync() + { + throw new NotImplementedException(); + } + + /// + /// Inject NosSmooth.Comms.Inject.dll into the process, + /// enable named pipes server and establish a connection to the server. + /// + /// The process to establish named pipes with. + /// The token used for stopping the connection. + /// The cancellation token used for cancelling the operation. + /// The result containing information about the established connection. + public async Task> 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.FromError(injectResult); + } + + var namedPipeClient = new NamedPipeClient($"NosSmooth_{process.Id}"); + + var connectionResult = await namedPipeClient.ConnectAsync(ct); + if (!connectionResult.IsSuccess) + { + return Result.FromError(connectionResult); + } + + var handler = ActivatorUtilities.CreateInstance + (_serviceProvider, (IConnection)namedPipeClient); + handler.StartHandler(stopToken); + + var nostaleClient = _resolver.Resolve(handler); + return new Comms(process, handler, nostaleClient); + } +} \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Local/Extensions/ServiceCollectionExtensions.cs b/src/Local/NosSmooth.Comms.Local/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..15c7dbb --- /dev/null +++ b/src/Local/NosSmooth.Comms.Local/Extensions/ServiceCollectionExtensions.cs @@ -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; + +/// +/// Extension methods for . +/// +public static class ServiceCollectionExtensions +{ + /// + /// Add . + /// + /// The service ocllection. + /// The same service collection. + public static IServiceCollection AddLocalComms(this IServiceCollection serviceCollection) + { + return serviceCollection + .AddMultiClientHandling() + .AddMessageResponder() + .AddMessageResponder() + .AddSingleton() + .AddSingleton(); + } +} \ No newline at end of file diff --git a/src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs b/src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs new file mode 100644 index 0000000..3692023 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs @@ -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; + +/// +/// Responds to deserialized packets. +/// +public class PacketResponder : IMessageResponder +{ + private readonly INostaleClient _client; + private readonly PacketHandler _packetHandler; + private readonly IPacketSerializer _serializer; + + /// + /// Initializes a new instance of the class. + /// + /// The nostale client. + /// The packet handler. + /// The serializer. + public PacketResponder(INostaleClient client, PacketHandler packetHandler, IPacketSerializer serializer) + { + _client = client; + _packetHandler = packetHandler; + _serializer = serializer; + } + + /// + public Task 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 diff --git a/src/Local/NosSmooth.Comms.Local/MessageResponders/RawPacketResponder.cs b/src/Local/NosSmooth.Comms.Local/MessageResponders/RawPacketResponder.cs new file mode 100644 index 0000000..3af1928 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Local/MessageResponders/RawPacketResponder.cs @@ -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; + +/// +/// Responds to raw packets. +/// +public class RawPacketResponder : IMessageResponder +{ + private readonly INostaleClient _client; + private readonly PacketHandler _packetHandler; + private readonly IPacketSerializer _serializer; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The nostale client. + /// The packet handler. + /// The serializer. + /// The logger. + public RawPacketResponder(INostaleClient client, PacketHandler packetHandler, IPacketSerializer serializer, ILogger logger) + { + _client = client; + _packetHandler = packetHandler; + _serializer = serializer; + _logger = logger; + } + + /// + public Task 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 diff --git a/src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj b/src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj new file mode 100644 index 0000000..0fb05e9 --- /dev/null +++ b/src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj @@ -0,0 +1,28 @@ + + + + net7.0 + enable + enable + + + + + ..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.localbinding\1.0.0\lib\net7.0\NosSmooth.LocalBinding.dll + + + ..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\remora.results\7.2.3\lib\net7.0\Remora.Results.dll + + + + + + + + + + + + + + diff --git a/src/Local/NosSmooth.Comms.LocalData/FocusMessage.cs b/src/Local/NosSmooth.Comms.LocalData/FocusMessage.cs new file mode 100644 index 0000000..b6e82b2 --- /dev/null +++ b/src/Local/NosSmooth.Comms.LocalData/FocusMessage.cs @@ -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 diff --git a/src/Local/NosSmooth.Comms.LocalData/FollowMessage.cs b/src/Local/NosSmooth.Comms.LocalData/FollowMessage.cs new file mode 100644 index 0000000..e4c99b9 --- /dev/null +++ b/src/Local/NosSmooth.Comms.LocalData/FollowMessage.cs @@ -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 diff --git a/src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj b/src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj new file mode 100644 index 0000000..6836c68 --- /dev/null +++ b/src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj @@ -0,0 +1,9 @@ + + + + net7.0 + enable + enable + + + -- 2.48.1