M NosSmooth.Comms.sln => NosSmooth.Comms.sln +2 -2
@@ 2,11 2,11 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Abstractions", "src\Core\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj", "{D3B6C5ED-9291-4215-8F8A-F3530849B896}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Local", "src\Local\NosSmooth.Comms.Local\NosSmooth.Comms.Local.csproj", "{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Inject", "src\Local\NosSmooth.Comms.Inject\NosSmooth.Comms.Inject.csproj", "{7EFDE13A-1D42-44FE-A752-EBE6AC25E1DC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.NamedPipes", "src\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj", "{9621D790-97FC-4E01-BB0C-CA1F33B4C934}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Injector", "src\Local\NosSmooth.Comms.Injector\NosSmooth.Comms.Injector.csproj", "{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NosSmooth.Comms.Local", "src\Local\NosSmooth.Comms.Local\NosSmooth.Comms.Local.csproj", "{A61B8A4F-EB81-41BC-8131-B45C0BCE0EA4}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{EFE65101-4414-4966-813D-90FE3736F6B1}"
EndProject
M src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs => src/Core/NosSmooth.Comms.Abstractions/Messages/HandshakeRequest.cs +1 -1
@@ 6,4 6,4 @@
namespace NosSmooth.Comms.Data.Messages;
-public record HandshakeRequest(bool SendRawPackets, bool SendDeserializedPackets);>
\ No newline at end of file
+public record HandshakeRequest(string Identification, bool SendRawPackets, bool SendDeserializedPackets);<
\ No newline at end of file
M src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj => src/Core/NosSmooth.Comms.Abstractions/NosSmooth.Comms.Abstractions.csproj +1 -0
@@ 9,6 9,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+ <PackageReference Include="NosSmooth.Core" Version="3.3.1" />
<PackageReference Include="NosSmooth.Packets" Version="3.5.0" />
<PackageReference Include="NosSmooth.PacketSerializer.Abstractions" Version="1.3.0" />
</ItemGroup>
M src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs => src/Core/NosSmooth.Comms.Core/ClientNostaleClient.cs +21 -16
@@ 8,8 8,8 @@ using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
+using NosSmooth.Core.Contracts;
using NosSmooth.Packets;
-using NosSmooth.PacketSerializer;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using Remora.Results;
@@ 42,40 42,45 @@ public class ClientNostaleClient : INostaleClient
/// <inheritdoc />
public async Task<Result> SendPacketAsync(IPacket packet, CancellationToken ct = default)
{
- var messageResponse = await _connection.SendMessageAsync
- (new PacketMessage(PacketSource.Client, packet), ct);
- return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
+ var messageResponse = await _connection.ContractSendMessage
+ (new PacketMessage(PacketSource.Client, packet))
+ .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
+ return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
}
/// <inheritdoc />
public async Task<Result> SendPacketAsync(string packetString, CancellationToken ct = default)
{
- var messageResponse = await _connection.SendMessageAsync
- (new RawPacketMessage(PacketSource.Client, packetString), ct);
- return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
+ var messageResponse = await _connection.ContractSendMessage
+ (new RawPacketMessage(PacketSource.Client, packetString))
+ .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
+ return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
}
/// <inheritdoc />
public async Task<Result> ReceivePacketAsync(string packetString, CancellationToken ct = default)
{
- var messageResponse = await _connection.SendMessageAsync
- (new RawPacketMessage(PacketSource.Server, packetString), ct);
- return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
+ var messageResponse = await _connection.ContractSendMessage
+ (new RawPacketMessage(PacketSource.Server, packetString))
+ .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
+ return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
}
/// <inheritdoc />
public async Task<Result> ReceivePacketAsync(IPacket packet, CancellationToken ct = default)
{
- var messageResponse = await _connection.SendMessageAsync
- (new PacketMessage(PacketSource.Server, packet), ct);
- return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
+ var messageResponse = await _connection.ContractSendMessage
+ (new PacketMessage(PacketSource.Server, packet))
+ .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
+ return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
}
/// <inheritdoc />
public async Task<Result> SendCommandAsync(ICommand command, CancellationToken ct = default)
{
- var messageResponse = await _connection.SendMessageAsync
- (new CommandMessage(command), ct);
- return messageResponse.IsSuccess ? Result.FromSuccess() : Result.FromError(messageResponse);
+ var messageResponse = await _connection.ContractSendMessage
+ (new CommandMessage(command))
+ .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
+ return messageResponse.IsSuccess ? messageResponse.Entity : Result.FromError(messageResponse);
}
}=
\ No newline at end of file
M src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs => src/Core/NosSmooth.Comms.Core/ConnectionHandler.cs +55 -2
@@ 4,6 4,7 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System.Data;
using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
@@ 51,9 52,20 @@ public class ConnectionHandler
_messageHandler = messageHandler;
_options = options.Value;
_logger = logger;
+ Id = Guid.NewGuid();
}
/// <summary>
+ /// Gets the id of the connection.
+ /// </summary>
+ public Guid Id { get; }
+
+ /// <summary>
+ /// The connection has been closed.
+ /// </summary>
+ public event EventHandler? Closed;
+
+ /// <summary>
/// Gets the connection.
/// </summary>
public IConnection Connection => _connection;
@@ 86,19 98,19 @@ public class ConnectionHandler
private async Task<Result> HandlerTask(CancellationToken ct)
{
using var reader = new MessagePackStreamReader(_connection.ReadStream, true);
- while (!ct.IsCancellationRequested)
+ while (!ct.IsCancellationRequested && _connection.State == ConnectionState.Open)
{
try
{
var read = await reader.ReadAsync(ct);
if (!read.HasValue)
{
- _logger.LogWarning("Message not read? ...");
continue;
}
var message = MessagePackSerializer.Typeless.Deserialize
(read.Value, _options, ct);
+
var result = await _messageHandler.HandleMessageAsync(this, message, ct);
if (!result.IsSuccess)
@@ 113,6 125,7 @@ public class ConnectionHandler
}
_connection.Disconnect();
+ Closed?.Invoke(this, EventArgs.Empty);
return Result.FromSuccess();
}
@@ 120,6 133,46 @@ public class ConnectionHandler
/// Create a contract for sending a message,
/// <see cref="ResponseResult"/> will be returned back.
/// </summary>
+ /// <param name="handshake">The handshake request.</param>
+ /// <typeparam name="TMessage">The type of the message.</typeparam>
+ /// <returns>A contract representing send message operation.</returns>
+ /// <exception cref="InvalidOperationException">Thrown in case contract is created on the server. Clients do not send responses.</exception>
+ public IContract<HandshakeResponse, DefaultStates> ContractHanshake(HandshakeRequest handshake)
+ {
+ if (_contractor is null)
+ {
+ throw new InvalidOperationException
+ (
+ "Contracting is not supported, the other side does not send responses. Only server sends responses back."
+ );
+ }
+
+ return new ContractBuilder<HandshakeResponse, DefaultStates, NoErrors>(_contractor, DefaultStates.None)
+ .SetMoveAction
+ (
+ DefaultStates.None,
+ async (a, ct) =>
+ {
+ var result = await SendMessageAsync(handshake, ct);
+ if (!result.IsDefined(out _))
+ {
+ return Result<bool>.FromError(result);
+ }
+
+ return true;
+ },
+ DefaultStates.Requested
+ )
+ .SetMoveFilter<HandshakeResponse>
+ (DefaultStates.Requested, DefaultStates.ResponseObtained)
+ .SetFillData<HandshakeResponse>(DefaultStates.ResponseObtained, r => r)
+ .Build();
+ }
+
+ /// <summary>
+ /// Create a contract for sending a message,
+ /// <see cref="ResponseResult"/> will be returned back.
+ /// </summary>
/// <param name="message">The message.</param>
/// <typeparam name="TMessage">The type of the message.</typeparam>
/// <returns>A contract representing send message operation.</returns>
A src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs => src/Core/NosSmooth.Comms.Core/Errors/MessageHandlerNotFoundError.cs +14 -0
@@ 0,0 1,14 @@
+//
+// MessageHandlerNotFoundError.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Remora.Results;
+
+namespace NosSmooth.Comms.Core.Errors;
+
+/// <summary>
+/// No message handler was found for the received message.
+/// </summary>
+public record MessageHandlerNotFoundError() : ResultError("Message handler for the given message was not found.");<
\ No newline at end of file
M src/Core/NosSmooth.Comms.Core/MessageHandler.cs => src/Core/NosSmooth.Comms.Core/MessageHandler.cs +24 -3
@@ 4,13 4,16 @@
// Copyright (c) František Boháček. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+using System.Diagnostics.Contracts;
using System.Reflection;
using System.Runtime.InteropServices.JavaScript;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using NosSmooth.Comms.Core.Errors;
using NosSmooth.Comms.Data;
using NosSmooth.Comms.Data.Messages;
using NosSmooth.Comms.Data.Responders;
+using NosSmooth.Core.Contracts;
using Remora.Results;
namespace NosSmooth.Comms.Core;
@@ 67,9 70,20 @@ public class MessageHandler
private async Task<Result> GenericHandleMessageAsync<TMessage>
(ConnectionHandler connection, MessageWrapper<TMessage> wrappedMessage, CancellationToken ct)
+ where TMessage : notnull
{
var data = wrappedMessage.Data;
+ var contractor = _services.GetService<Contractor>();
+ if (contractor is not null)
+ {
+ var contractorResult = await contractor.Update(wrappedMessage.Data, ct);
+ if (!contractorResult.IsSuccess)
+ {
+ return contractorResult;
+ }
+ }
+
await using var scope = _services.CreateAsyncScope();
var injector = scope.ServiceProvider.GetRequiredService<ConnectionInjector>();
injector.ConnectionHandler = connection;
@@ 77,7 91,8 @@ public class MessageHandler
var responders = scope.ServiceProvider
.GetServices<IMessageResponder<TMessage>>()
- .Select(x => x.Respond(data, ct));
+ .Select(x => x.Respond(data, ct))
+ .ToArray();
var results = (await Task.WhenAll(responders))
.Where(x => !x.IsSuccess)
@@ 93,11 108,17 @@ public class MessageHandler
if (_respond && wrappedMessage.Data is not ResponseResult)
{
- var response = new ResponseResult(wrappedMessage.MessageId, result);
+ var responseResult = result;
+ if (responders.Length == 0)
+ {
+ responseResult = new MessageHandlerNotFoundError();
+ }
+
+ var response = new ResponseResult(wrappedMessage.MessageId, responseResult);
var sentMessageResult = await connection.SendMessageAsync(response, ct);
if (!sentMessageResult.IsSuccess)
{
- results.Add(sentMessageResult);
+ results.Add(Result.FromError(sentMessageResult));
result = results.Count switch
{
0 => Result.FromSuccess(),
M src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj => src/Core/NosSmooth.Comms.Core/NosSmooth.Comms.Core.csproj +1 -0
@@ 22,6 22,7 @@
</ItemGroup>
<ItemGroup>
+ <PackageReference Include="MessagePack" Version="2.4.59" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
<PackageReference Include="NosSmooth.Core" Version="3.3.1" />
</ItemGroup>
M src/Core/NosSmooth.Comms.Core/ServerManager.cs => src/Core/NosSmooth.Comms.Core/ServerManager.cs +65 -4
@@ 7,7 7,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NosSmooth.Comms.Data;
-using NosSmooth.Comms.Data.Messages;
using NosSmooth.Core.Extensions;
using Remora.Results;
@@ 24,6 23,7 @@ public class ServerManager
private readonly ILogger<ServerManager> _logger;
private readonly ILogger<ConnectionHandler> _handlerLogger;
private readonly List<ConnectionHandler> _connectionHandlers;
+ private readonly ReaderWriterLockSlim _readerWriterLock;
private Task<Result>? _task;
private CancellationTokenSource? _ctSource;
@@ 46,6 46,7 @@ public class ServerManager
{
_server = server;
_connectionHandlers = new List<ConnectionHandler>();
+ _readerWriterLock = new ReaderWriterLockSlim();
_messageHandler = messageHandler;
_options = options;
_logger = logger;
@@ 53,6 54,25 @@ public class ServerManager
}
/// <summary>
+ /// Gets connection handlers.
+ /// </summary>
+ public IReadOnlyList<ConnectionHandler> ConnectionHandlers
+ {
+ get
+ {
+ _readerWriterLock.EnterReadLock();
+ try
+ {
+ return _connectionHandlers.ToArray();
+ }
+ finally
+ {
+ _readerWriterLock.ExitReadLock();
+ }
+ }
+ }
+
+ /// <summary>
/// Run the manager and await the task.
/// </summary>
/// <param name="stopToken">The token used for stopping the handler and disconnecting the connection.</param>
@@ 131,14 151,45 @@ public class ServerManager
continue;
}
- var handler = new ConnectionHandler(null, connection, _messageHandler, _options, _handlerLogger);
- _connectionHandlers.Add(handler);
+ var handler = new ConnectionHandler
+ (
+ null,
+ connection,
+ _messageHandler,
+ _options,
+ _handlerLogger
+ );
+ _readerWriterLock.EnterWriteLock();
+
+ try
+ {
+ _connectionHandlers.Add(handler);
+ }
+ finally
+ {
+ _readerWriterLock.ExitWriteLock();
+ }
+ handler.Closed += (o, e) =>
+ {
+ _logger.LogInformation("A connection ({ConnectionId}) has been closed", handler.Id);
+ _readerWriterLock.EnterWriteLock();
+ try
+ {
+ _connectionHandlers.Remove(handler);
+ }
+ finally
+ {
+ _readerWriterLock.ExitWriteLock();
+ }
+ };
+
+ _logger.LogInformation("A connection ({ConnectionId}) has been established", handler.Id);
handler.StartHandler(_ctSource.Token);
}
List<IResult> errors = new List<IResult>();
- foreach (var handler in _connectionHandlers)
+ foreach (var handler in ConnectionHandlers)
{
var handlerResult = await handler.RunHandlerAsync(_ctSource.Token);
@@ 148,6 199,16 @@ public class ServerManager
}
}
+ _readerWriterLock.EnterWriteLock();
+ try
+ {
+ _connectionHandlers.Clear();
+ }
+ finally
+ {
+ _readerWriterLock.ExitWriteLock();
+ }
+
_server.Close();
return errors.Count switch
{
M src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs => src/Core/NosSmooth.Comms.NamedPipes/NamedPipeServer.cs +35 -11
@@ 6,6 6,7 @@
using System.Data;
using System.IO.Pipes;
+using System.Xml;
using NosSmooth.Comms.Data;
using Remora.Results;
@@ 38,9 39,14 @@ public class NamedPipeServer : IServer
get
{
_readerWriterLock.EnterReadLock();
- var connections = new List<IConnection>(_connections);
- _readerWriterLock.ExitReadLock();
- return connections.AsReadOnly();
+ try
+ {
+ return _connections.ToArray();
+ }
+ finally
+ {
+ _readerWriterLock.ExitReadLock();
+ }
}
}
@@ 65,8 71,14 @@ public class NamedPipeServer : IServer
var connection = new NamedPipeConnection(this, serverStream);
_readerWriterLock.EnterWriteLock();
- _connections.Add(connection);
- _readerWriterLock.ExitWriteLock();
+ try
+ {
+ _connections.Add(connection);
+ }
+ finally
+ {
+ _readerWriterLock.ExitWriteLock();
+ }
return connection;
}
@@ 83,8 95,15 @@ public class NamedPipeServer : IServer
public void Close()
{
_readerWriterLock.EnterReadLock();
- var connections = new List<IConnection>(_connections);
- _readerWriterLock.ExitReadLock();
+ IReadOnlyList<IConnection> connections;
+ try
+ {
+ connections = new List<IConnection>(_connections);
+ }
+ finally
+ {
+ _readerWriterLock.ExitReadLock();
+ }
foreach (var connection in connections)
{
@@ 105,7 124,7 @@ public class NamedPipeServer : IServer
_serverStream = serverStream;
}
- public ConnectionState State { get; private set; } = ConnectionState.Open;
+ public ConnectionState State => _serverStream.IsConnected ? ConnectionState.Open : ConnectionState.Closed;
public Stream ReadStream => _serverStream;
@@ 115,11 134,16 @@ public class NamedPipeServer : IServer
{
_serverStream.Disconnect();
_serverStream.Close();
- State = ConnectionState.Closed;
_server._readerWriterLock.EnterWriteLock();
- _server._connections.Remove(this);
- _server._readerWriterLock.ExitWriteLock();
+ try
+ {
+ _server._connections.Remove(this);
+ }
+ finally
+ {
+ _server._readerWriterLock.ExitWriteLock();
+ }
}
}
}=
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/CallbackConfig.cs => src/Local/NosSmooth.Comms.Inject/CallbackConfig.cs +9 -0
@@ 0,0 1,9 @@
+//
+// CallbackConfig.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Comms.Inject;
+
+public record CallbackConfig(bool SendRawPackets, bool SendDeserializedPackets);<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs => src/Local/NosSmooth.Comms.Inject/CallbackConfigRepository.cs +46 -0
@@ 0,0 1,46 @@
+//
+// CallbackConfigRepository.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Collections.Concurrent;
+using NosSmooth.Comms.Core;
+
+namespace NosSmooth.Comms.Inject;
+
+/// <summary>
+/// A repository containing configurations for given connection handlers.
+/// </summary>
+public class CallbackConfigRepository
+{
+ private readonly ConcurrentDictionary<ConnectionHandler, CallbackConfig> _configs;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CallbackConfigRepository"/> class.
+ /// </summary>
+ public CallbackConfigRepository()
+ {
+ _configs = new ConcurrentDictionary<ConnectionHandler, CallbackConfig>();
+ }
+
+ /// <summary>
+ /// Get config of the given connection, or default.
+ /// </summary>
+ /// <param name="connection">The connection to get config of.</param>
+ /// <returns>A config for the connection.</returns>
+ public CallbackConfig GetConfig(ConnectionHandler connection)
+ {
+ return _configs.GetValueOrDefault(connection, new CallbackConfig(false, false));
+ }
+
+ /// <summary>
+ /// Set config of the given connection.
+ /// </summary>
+ /// <param name="connection">The connection to set config.</param>
+ /// <param name="config">The config to set.</param>
+ public void SetConfig(ConnectionHandler connection, CallbackConfig config)
+ {
+ _configs.AddOrUpdate(connection, _ => config, (a, b) => config);
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/DllMain.cs => src/Local/NosSmooth.Comms.Inject/DllMain.cs +129 -0
@@ 0,0 1,129 @@
+//
+// DllMain.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Comms.Core;
+using NosSmooth.Comms.Core.Extensions;
+using NosSmooth.Comms.Data;
+using NosSmooth.Comms.Inject.MessageResponders;
+using NosSmooth.Comms.Inject.PacketResponders;
+using NosSmooth.Comms.NamedPipes;
+using NosSmooth.Comms.NamedPipes.Extensions;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Extensions.SharedBinding.Extensions;
+using NosSmooth.LocalClient.Extensions;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Inject;
+
+/// <summary>
+/// A main entrypoint to NosSmooth local communications.
+/// </summary>
+public class DllMain
+{
+ private static IHost? _host;
+
+ /// <summary>
+ /// Allocate console.
+ /// </summary>
+ /// <returns>Whether the operation was successful.</returns>
+ [DllImport("kernel32")]
+ public static extern bool AllocConsole();
+
+ /// <summary>
+ /// Enable named pipes server.
+ /// </summary>
+ [UnmanagedCallersOnly(EntryPoint = "EnableNamedPipes")]
+ public static void EnableNamedPipes()
+ {
+ Main
+ (
+ host =>
+ {
+ var manager = host.Services.GetRequiredService<ServerManager>();
+ return manager.RunManagerAsync(host.Services.GetRequiredService<IHostApplicationLifetime>().ApplicationStopping);
+ }
+ );
+ }
+
+ private static void Main(Func<IHost, Task<Result>> host)
+ {
+ AllocConsole();
+ new Thread
+ (
+ () =>
+ {
+ try
+ {
+ MainEntry(host).GetAwaiter().GetResult();
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.ToString());
+ }
+ }
+ ).Start();
+ }
+
+ /// <summary>
+ /// The entrypoint method.
+ /// </summary>
+ /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+ private static async Task MainEntry(Func<IHost, Task<Result>> host)
+ {
+ if (_host is not null)
+ {
+ var result = await host(_host);
+ if (!result.IsSuccess)
+ {
+ _host.Services.GetRequiredService<ILogger<DllMain>>().LogResultError(result);
+ }
+ return;
+ }
+
+ _host = Host.CreateDefaultBuilder()
+ .UseConsoleLifetime()
+ .ConfigureLogging
+ (
+ b =>
+ {
+ b
+ .ClearProviders()
+ .AddConsole();
+ }
+ )
+ .ConfigureServices
+ (
+ s =>
+ {
+ s
+ .AddSingleton<CallbackConfigRepository>()
+ .AddNostaleCore()
+ .AddLocalClient()
+ .ShareNosSmooth()
+ .AddNamedPipeServer(p => $"NosSmooth_{Process.GetCurrentProcess().Id}")
+ .AddPacketResponder<EveryPacketResponder>()
+ .AddServerHandling()
+ .AddMessageResponder<CommandResponder>()
+ .AddMessageResponder<FocusResponder>()
+ .AddMessageResponder<FollowResponder>()
+ .AddMessageResponder<HandshakeResponder>()
+ .AddMessageResponder<PacketResponder>();
+ s.AddHostedService<NosSmoothService>();
+ }
+ ).Build();
+
+ await _host.StartAsync();
+ var hostTask = _host.RunAsync();
+ var serverTask = host(_host);
+
+ await Task.WhenAll(hostTask, serverTask);
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/CommandResponder.cs +34 -0
@@ 0,0 1,34 @@
+//
+// CommandResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Comms.Data.Messages;
+using NosSmooth.Comms.Data.Responders;
+using NosSmooth.Core.Client;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Inject.MessageResponders;
+
+/// <summary>
+/// A responder to <see cref="CommandMessage"/>.
+/// </summary>
+public class CommandResponder : IMessageResponder<CommandMessage>
+{
+ private readonly INostaleClient _client;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CommandResponder"/> class.
+ /// </summary>
+ /// <param name="client">The client.</param>
+ public CommandResponder(INostaleClient client)
+ {
+ _client = client;
+
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(CommandMessage message, CancellationToken ct = default)
+ => _client.SendCommandAsync(message.Command, ct);
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/FocusResponder.cs +63 -0
@@ 0,0 1,63 @@
+//
+// FocusResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Comms.Data.Responders;
+using NosSmooth.Comms.LocalData;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalBinding.Hooks;
+using NosSmooth.LocalBinding.Structs;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Inject.MessageResponders;
+
+/// <summary>
+/// A resopnder to <see cref="FocusResponder"/>.
+/// </summary>
+public class FocusResponder : IMessageResponder<FocusMessage>
+{
+ private readonly NosBrowserManager _browserManager;
+ private readonly NosThreadSynchronizer _synchronizer;
+ private readonly IEntityFocusHook _focusHook;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FocusResponder"/> class.
+ /// </summary>
+ /// <param name="browserManager">The browser manager.</param>
+ /// <param name="synchronizer">The synchronizer.</param>
+ /// <param name="focusHook">The focus hook.</param>
+ public FocusResponder
+ (NosBrowserManager browserManager, NosThreadSynchronizer synchronizer, IEntityFocusHook focusHook)
+ {
+ _browserManager = browserManager;
+ _synchronizer = synchronizer;
+ _focusHook = focusHook;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(FocusMessage message, CancellationToken ct = default)
+ {
+ MapBaseObj? entity = null;
+ if (message.EntityId is not null)
+ {
+ var entityResult = _browserManager.SceneManager.FindEntity(message.EntityId.Value);
+
+ if (!entityResult.IsDefined(out entity))
+ {
+ return Result.FromError(new NotFoundError($"Entity with id {message.EntityId} not found."));
+ }
+ }
+
+ return await _synchronizer.SynchronizeAsync
+ (
+ () =>
+ {
+ _focusHook.WrapperFunction(entity);
+ return Result.FromSuccess();
+ },
+ ct
+ );
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/FollowResponder.cs +70 -0
@@ 0,0 1,70 @@
+//
+// FollowResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Comms.Data.Responders;
+using NosSmooth.Comms.LocalData;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalBinding.Hooks;
+using NosSmooth.LocalBinding.Structs;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Inject.MessageResponders;
+
+/// <summary>
+/// A responder to <see cref="FollowMessage"/>.
+/// </summary>
+public class FollowResponder : IMessageResponder<FollowMessage>
+{
+ private readonly NosBrowserManager _browserManager;
+ private readonly NosThreadSynchronizer _synchronizer;
+ private readonly IHookManager _hookManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="FollowResponder"/> class.
+ /// </summary>
+ /// <param name="browserManager">The browser manager.</param>
+ /// <param name="synchronizer">The synchronizer.</param>
+ /// <param name="hookManager">The hook manager.</param>
+ public FollowResponder
+ (NosBrowserManager browserManager, NosThreadSynchronizer synchronizer, IHookManager hookManager)
+ {
+ _browserManager = browserManager;
+ _synchronizer = synchronizer;
+ _hookManager = hookManager;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(FollowMessage message, CancellationToken ct = default)
+ {
+ MapBaseObj? entity = null;
+ if (message.EntityId is not null)
+ {
+ var entityResult = _browserManager.SceneManager.FindEntity(message.EntityId.Value);
+
+ if (!entityResult.IsDefined(out entity))
+ {
+ return Result.FromError(new NotFoundError($"Entity with id {message.EntityId} not found."));
+ }
+ }
+
+ return await _synchronizer.SynchronizeAsync
+ (
+ () =>
+ {
+ if (entity is null)
+ {
+ _hookManager.EntityUnfollow.WrapperFunction();
+ }
+ else
+ {
+ _hookManager.EntityFollow.WrapperFunction(entity);
+ }
+ return Result.FromSuccess();
+ },
+ ct
+ );
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/HandshakeResponder.cs +77 -0
@@ 0,0 1,77 @@
+//
+// HandshakeResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.Logging;
+using NosSmooth.Comms.Core;
+using NosSmooth.Comms.Data.Messages;
+using NosSmooth.Comms.Data.Responders;
+using NosSmooth.LocalBinding;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Inject.MessageResponders;
+
+/// <summary>
+/// A responder to <see cref="HandshakeRequest"/>.
+/// </summary>
+public class HandshakeResponder : IMessageResponder<HandshakeRequest>
+{
+ private readonly NosBrowserManager _browserManager;
+ private readonly ConnectionHandler _connectionHandler;
+ private readonly CallbackConfigRepository _config;
+ private readonly ILogger<HandshakeResponder> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="HandshakeResponder"/> class.
+ /// </summary>
+ /// <param name="browserManager">The browser manager.</param>
+ /// <param name="connectionHandler">The connection handler.</param>
+ /// <param name="config">The config.</param>
+ /// <param name="logger">The logger.</param>
+ public HandshakeResponder
+ (
+ NosBrowserManager browserManager,
+ ConnectionHandler connectionHandler,
+ CallbackConfigRepository config,
+ ILogger<HandshakeResponder> logger
+ )
+ {
+ _browserManager = browserManager;
+ _connectionHandler = connectionHandler;
+ _config = config;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond(HandshakeRequest message, CancellationToken ct = default)
+ {
+ var config = new CallbackConfig(message.SendRawPackets, message.SendDeserializedPackets);
+ _config.SetConfig(_connectionHandler, config);
+
+ string? playerName = null;
+ long? playerId = null;
+
+ if (_browserManager.IsInGame)
+ {
+ playerName = _browserManager.PlayerManager.Player.Name;
+ playerId = _browserManager.PlayerManager.PlayerId;
+ }
+
+ var result = await _connectionHandler.SendMessageAsync
+ (
+ new HandshakeResponse(playerId, playerName),
+ ct
+ );
+
+ _logger.LogInformation
+ (
+ "Handshaked with {Identification}! (connection {ConnectionID})",
+ message.Identification,
+ _connectionHandler.Id
+ );
+
+ return result.IsSuccess ? Result.FromSuccess() : Result.FromError(result);
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs => src/Local/NosSmooth.Comms.Inject/MessageResponders/PacketResponder.cs +52 -0
@@ 0,0 1,52 @@
+//
+// PacketResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Comms.Data.Messages;
+using NosSmooth.Comms.Data.Responders;
+using NosSmooth.Core.Client;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Inject.MessageResponders;
+
+/// <summary>
+/// A responder to <see cref="RawPacketMessage"/> and <see cref="PacketMessage"/>.
+/// </summary>
+public class PacketResponder : IMessageResponder<RawPacketMessage>, IMessageResponder<PacketMessage>
+{
+ private readonly INostaleClient _client;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PacketResponder"/> class.
+ /// </summary>
+ /// <param name="client">The NosTale client.</param>
+ public PacketResponder(INostaleClient client)
+ {
+ _client = client;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(RawPacketMessage message, CancellationToken ct = default)
+ {
+ if (message.Source == PacketSource.Client)
+ {
+ return _client.SendPacketAsync(message.Packet, ct);
+ }
+
+ return _client.ReceivePacketAsync(message.Packet, ct);
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketMessage message, CancellationToken ct = default)
+ {
+ if (message.Source == PacketSource.Client)
+ {
+ return _client.SendPacketAsync(message.Packet, ct);
+ }
+
+ return _client.ReceivePacketAsync(message.Packet, ct);
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj => src/Local/NosSmooth.Comms.Inject/NosSmooth.Comms.Inject.csproj +26 -0
@@ 0,0 1,26 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
+ <PackageReference Include="NosSmooth.Extensions.SharedBinding" Version="0.0.1" />
+ <PackageReference Include="NosSmooth.LocalBinding" Version="1.0.0" />
+ <PackageReference Include="NosSmooth.LocalClient" Version="1.0.0" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Core\NosSmooth.Comms.Abstractions\NosSmooth.Comms.Abstractions.csproj" />
+ <ProjectReference Include="..\..\Core\NosSmooth.Comms.Core\NosSmooth.Comms.Core.csproj" />
+ <ProjectReference Include="..\..\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj" />
+ <ProjectReference Include="..\NosSmooth.Comms.LocalData\NosSmooth.Comms.LocalData.csproj" />
+ </ItemGroup>
+
+</Project>
A src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs => src/Local/NosSmooth.Comms.Inject/NosSmoothService.cs +79 -0
@@ 0,0 1,79 @@
+//
+// NosSmoothService.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Extensions;
+using NosSmooth.LocalBinding;
+using NosSmooth.PacketSerializer.Extensions;
+using NosSmooth.PacketSerializer.Packets;
+
+namespace NosSmooth.Comms.Inject;
+
+/// <summary>
+/// Nostale client runner.
+/// </summary>
+public class NosSmoothService : BackgroundService
+{
+ private readonly IServiceProvider _services;
+ private readonly IPacketTypesRepository _packetTypesRepository;
+ private readonly NosBindingManager _bindingManager;
+ private readonly IHostLifetime _lifetime;
+ private readonly ILogger<NosSmoothService> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="NosSmoothService"/> class.
+ /// </summary>
+ /// <param name="services">The services.</param>
+ /// <param name="packetTypesRepository">The packet types repository.</param>
+ /// <param name="bindingManager">The binding manager.</param>
+ /// <param name="lifetime">The lifetime.</param>
+ /// <param name="logger">The logger.</param>
+ public NosSmoothService
+ (
+ IServiceProvider services,
+ IPacketTypesRepository packetTypesRepository,
+ NosBindingManager bindingManager,
+ IHostLifetime lifetime,
+ ILogger<NosSmoothService> logger
+ )
+ {
+ _services = services;
+ _packetTypesRepository = packetTypesRepository;
+ _bindingManager = bindingManager;
+ _lifetime = lifetime;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ var packetResult = _packetTypesRepository.AddDefaultPackets();
+ if (!packetResult.IsSuccess)
+ {
+ _logger.LogResultError(packetResult);
+ return;
+ }
+
+ var bindingResult = _bindingManager.Initialize();
+ if (!bindingResult.IsSuccess)
+ {
+ _logger.LogResultError(bindingResult);
+ return;
+ }
+
+ var nostaleClient = _services.GetRequiredService<INostaleClient>();
+ var runResult = await nostaleClient.RunAsync(stoppingToken);
+ if (!runResult.IsSuccess)
+ {
+ _logger.LogResultError(runResult);
+ await _lifetime.StopAsync(default);
+ }
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs => src/Local/NosSmooth.Comms.Inject/PacketResponders/EveryPacketResponder.cs +70 -0
@@ 0,0 1,70 @@
+//
+// EveryPacketResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Comms.Core;
+using NosSmooth.Comms.Data.Messages;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Inject.PacketResponders;
+
+/// <inheritdoc />
+public class EveryPacketResponder : IEveryPacketResponder
+{
+ private readonly CallbackConfigRepository _config;
+ private readonly ServerManager _serverManager;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="EveryPacketResponder"/> class.
+ /// </summary>
+ /// <param name="config">The configuration.</param>
+ /// <param name="serverManager">The server manager.</param>
+ public EveryPacketResponder(CallbackConfigRepository config, ServerManager serverManager)
+ {
+ _config = config;
+ _serverManager = serverManager;
+ }
+
+ /// <inheritdoc />
+ public async Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
+ where TPacket : IPacket
+ {
+ var errors = new List<IResult>();
+
+ foreach (var connectionHandler in _serverManager.ConnectionHandlers)
+ {
+ var config = _config.GetConfig(connectionHandler);
+ if (config.SendRawPackets)
+ {
+ var result = await connectionHandler.SendMessageAsync
+ (new RawPacketMessage(packetArgs.Source, packetArgs.PacketString), ct);
+
+ if (!result.IsSuccess)
+ {
+ errors.Add(Result.FromError(result));
+ }
+ }
+
+ if (config.SendDeserializedPackets)
+ {
+ var result = await connectionHandler.SendMessageAsync(new PacketMessage(packetArgs.Source, packetArgs.Packet), ct);
+
+ if (!result.IsSuccess)
+ {
+ errors.Add(Result.FromError(result));
+ }
+ }
+ }
+
+ return errors.Count switch
+ {
+ 0 => Result.FromSuccess(),
+ 1 => (Result)errors[0],
+ _ => new AggregateError(errors)
+ };
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Local/Comms.cs => src/Local/NosSmooth.Comms.Local/Comms.cs +13 -0
@@ 0,0 1,13 @@
+//
+// Comms.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using NosSmooth.Comms.Core;
+using NosSmooth.Core.Client;
+
+namespace NosSmooth.Comms.Local;
+
+public record Comms(Process NosTaleProcess, ConnectionHandler Connection, INostaleClient Client);<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Local/CommsInjector.cs => src/Local/NosSmooth.Comms.Local/CommsInjector.cs +154 -0
@@ 0,0 1,154 @@
+//
+// CommsInjector.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System.Diagnostics;
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Comms.Core;
+using NosSmooth.Comms.Data;
+using NosSmooth.Comms.NamedPipes;
+using NosSmooth.Injector;
+using NosSmooth.LocalBinding;
+using NosSmooth.LocalBinding.Options;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Local;
+
+/// <summary>
+/// Injects communication (tcp or named pipes) into a nostale process.
+/// </summary>
+public class CommsInjector
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly NosInjector _injector;
+ private readonly NostaleClientResolver _resolver;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CommsInjector"/> class.
+ /// </summary>
+ /// <param name="serviceProvider">The service provider.</param>
+ /// <param name="injector">The injector.</param>
+ /// <param name="resolver">The nostale client resolver.</param>
+ public CommsInjector(IServiceProvider serviceProvider, NosInjector injector, NostaleClientResolver resolver)
+ {
+ _serviceProvider = serviceProvider;
+ _injector = injector;
+ _resolver = resolver;
+ }
+
+ /// <summary>
+ /// Find processes that are NosTale and create a <see cref="NosBrowserManager"/> from them.
+ /// </summary>
+ /// <param name="filterNames">The names to filter when searching the processes. In case the array is empty, look for all processes.</param>
+ /// <returns>A list of the NosTale processes.</returns>
+ public static IEnumerable<Result<NosBrowserManager>> CreateNostaleProcesssesBrowsers(params string[] filterNames)
+ {
+ return FindNosTaleProcesses(filterNames)
+ .Select
+ (
+ x =>
+ {
+ var manager = new NosBrowserManager
+ (
+ x,
+ new PlayerManagerOptions(),
+ new SceneManagerOptions(),
+ new PetManagerOptions(),
+ new NetworkManagerOptions(),
+ new UnitManagerOptions()
+ );
+
+ var initResult = manager.Initialize();
+ if (!initResult.IsSuccess)
+ {
+ return Result<NosBrowserManager>.FromError(initResult.Error);
+ }
+
+ return manager;
+ }
+ );
+ }
+
+ /// <summary>
+ /// Find processes that are NosTale.
+ /// </summary>
+ /// <param name="filterNames">The names to filter when searching the processes. In case the array is empty, look for all processes.</param>
+ /// <returns>A list of the NosTale processes.</returns>
+ public static IEnumerable<Process> FindNosTaleProcesses(params string[] filterNames)
+ {
+ var processes = Process.GetProcesses().AsEnumerable();
+
+ if (filterNames.Length > 0)
+ {
+ processes = processes.Where(x => filterNames.Contains(x.ProcessName));
+ }
+
+ return processes
+ .Where
+ (
+ x =>
+ {
+ try
+ {
+ return NosBrowserManager.IsProcessNostaleProcess(x);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ );
+ }
+
+ /// <summary>
+ /// Inject NosSmooth.Comms.Inject.dll into the process,
+ /// enable tcp server and establish a connection to the server.
+ /// </summary>
+ /// <returns>The result containing information about the established connection.</returns>
+ public Task<Result<Comms>>
+ EstablishTcpConnectionAsync()
+ {
+ throw new NotImplementedException();
+ }
+
+ /// <summary>
+ /// Inject NosSmooth.Comms.Inject.dll into the process,
+ /// enable named pipes server and establish a connection to the server.
+ /// </summary>
+ /// <param name="process">The process to establish named pipes with.</param>
+ /// <param name="stopToken">The token used for stopping the connection.</param>
+ /// <param name="ct">The cancellation token used for cancelling the operation.</param>
+ /// <returns>The result containing information about the established connection.</returns>
+ public async Task<Result<Comms>> EstablishNamedPipesConnectionAsync
+ (Process process, CancellationToken stopToken, CancellationToken ct)
+ {
+ var injectResult = _injector.Inject
+ (
+ process,
+ Path.GetFullPath("NosSmooth.Comms.Inject.dll"),
+ "NosSmooth.Comms.Inject.DllMain, NosSmooth.Comms.Inject",
+ "EnableNamedPipes"
+ );
+ if (!injectResult.IsSuccess)
+ {
+ return Result<Comms>.FromError(injectResult);
+ }
+
+ var namedPipeClient = new NamedPipeClient($"NosSmooth_{process.Id}");
+
+ var connectionResult = await namedPipeClient.ConnectAsync(ct);
+ if (!connectionResult.IsSuccess)
+ {
+ return Result<Comms>.FromError(connectionResult);
+ }
+
+ var handler = ActivatorUtilities.CreateInstance<ConnectionHandler>
+ (_serviceProvider, (IConnection)namedPipeClient);
+ handler.StartHandler(stopToken);
+
+ var nostaleClient = _resolver.Resolve(handler);
+ return new Comms(process, handler, nostaleClient);
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Local/Extensions/ServiceCollectionExtensions.cs => src/Local/NosSmooth.Comms.Local/Extensions/ServiceCollectionExtensions.cs +33 -0
@@ 0,0 1,33 @@
+//
+// ServiceCollectionExtensions.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.DependencyInjection;
+using NosSmooth.Comms.Core.Extensions;
+using NosSmooth.Comms.Local.MessageResponders;
+using NosSmooth.Injector;
+
+namespace NosSmooth.Comms.Local.Extensions;
+
+/// <summary>
+/// Extension methods for <see cref="IServiceCollection"/>.
+/// </summary>
+public static class ServiceCollectionExtensions
+{
+ /// <summary>
+ /// Add <see cref="CommsInjector"/>.
+ /// </summary>
+ /// <param name="serviceCollection">The service ocllection.</param>
+ /// <returns>The same service collection.</returns>
+ public static IServiceCollection AddLocalComms(this IServiceCollection serviceCollection)
+ {
+ return serviceCollection
+ .AddMultiClientHandling()
+ .AddMessageResponder<PacketResponder>()
+ .AddMessageResponder<RawPacketResponder>()
+ .AddSingleton<NosInjector>()
+ .AddSingleton<CommsInjector>();
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs => src/Local/NosSmooth.Comms.Local/MessageResponders/PacketResponder.cs +56 -0
@@ 0,0 1,56 @@
+//
+// PacketResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using NosSmooth.Comms.Data.Messages;
+using NosSmooth.Comms.Data.Responders;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Packets;
+using NosSmooth.PacketSerializer;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Local.MessageResponders;
+
+/// <summary>
+/// Responds to deserialized packets.
+/// </summary>
+public class PacketResponder : IMessageResponder<PacketMessage>
+{
+ private readonly INostaleClient _client;
+ private readonly PacketHandler _packetHandler;
+ private readonly IPacketSerializer _serializer;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PacketResponder"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ /// <param name="packetHandler">The packet handler.</param>
+ /// <param name="serializer">The serializer.</param>
+ public PacketResponder(INostaleClient client, PacketHandler packetHandler, IPacketSerializer serializer)
+ {
+ _client = client;
+ _packetHandler = packetHandler;
+ _serializer = serializer;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(PacketMessage message, CancellationToken ct = default)
+ {
+ var serializedResult = _serializer.Serialize(message.Packet);
+ if (!serializedResult.IsDefined(out var serialized))
+ {
+ return Task.FromResult(Result.FromError(serializedResult));
+ }
+
+ return _packetHandler.HandlePacketAsync
+ (
+ _client,
+ message.Source,
+ message.Packet,
+ serialized,
+ ct
+ );
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Local/MessageResponders/RawPacketResponder.cs => src/Local/NosSmooth.Comms.Local/MessageResponders/RawPacketResponder.cs +78 -0
@@ 0,0 1,78 @@
+//
+// RawPacketResponder.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Extensions.Logging;
+using NosSmooth.Comms.Data.Messages;
+using NosSmooth.Comms.Data.Responders;
+using NosSmooth.Core.Client;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets;
+using NosSmooth.PacketSerializer;
+using NosSmooth.PacketSerializer.Errors;
+using Remora.Results;
+
+namespace NosSmooth.Comms.Local.MessageResponders;
+
+/// <summary>
+/// Responds to raw packets.
+/// </summary>
+public class RawPacketResponder : IMessageResponder<RawPacketMessage>
+{
+ private readonly INostaleClient _client;
+ private readonly PacketHandler _packetHandler;
+ private readonly IPacketSerializer _serializer;
+ private readonly ILogger<RawPacketResponder> _logger;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="RawPacketResponder"/> class.
+ /// </summary>
+ /// <param name="client">The nostale client.</param>
+ /// <param name="packetHandler">The packet handler.</param>
+ /// <param name="serializer">The serializer.</param>
+ /// <param name="logger">The logger.</param>
+ public RawPacketResponder(INostaleClient client, PacketHandler packetHandler, IPacketSerializer serializer, ILogger<RawPacketResponder> logger)
+ {
+ _client = client;
+ _packetHandler = packetHandler;
+ _serializer = serializer;
+ _logger = logger;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond(RawPacketMessage message, CancellationToken ct = default)
+ {
+ var deserializedResult = _serializer.Deserialize(message.Packet, message.Source);
+ IPacket packet;
+
+ if (!deserializedResult.IsSuccess)
+ {
+ if (deserializedResult.Error is not PacketConverterNotFoundError)
+ {
+ _logger.LogWarning("Could not parse {Packet}. Reason:", message.Packet);
+ _logger.LogResultError(deserializedResult);
+ packet = new ParsingFailedPacket(deserializedResult, message.Packet);
+ }
+ else
+ {
+ packet = new UnresolvedPacket(message.Packet.Split(' ')[0], message.Packet);
+ }
+ }
+ else
+ {
+ packet = deserializedResult.Entity;
+ }
+
+ return _packetHandler.HandlePacketAsync
+ (
+ _client,
+ message.Source,
+ packet,
+ message.Packet,
+ ct
+ );
+ }
+}<
\ No newline at end of file
A src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj => src/Local/NosSmooth.Comms.Local/NosSmooth.Comms.Local.csproj +28 -0
@@ 0,0 1,28 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <Reference Include="NosSmooth.LocalBinding">
+ <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\nossmooth.localbinding\1.0.0\lib\net7.0\NosSmooth.LocalBinding.dll</HintPath>
+ </Reference>
+ <Reference Include="Remora.Results">
+ <HintPath>..\..\..\..\..\..\..\..\..\ruther\.nuget\packages\remora.results\7.2.3\lib\net7.0\Remora.Results.dll</HintPath>
+ </Reference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\..\Core\NosSmooth.Comms.Core\NosSmooth.Comms.Core.csproj" />
+ <ProjectReference Include="..\..\Core\NosSmooth.Comms.NamedPipes\NosSmooth.Comms.NamedPipes.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <PackageReference Include="NosSmooth.Injector" Version="1.0.0" />
+ <PackageReference Include="NosSmooth.LocalBinding" Version="1.0.0" />
+ </ItemGroup>
+
+</Project>
A src/Local/NosSmooth.Comms.LocalData/FocusMessage.cs => src/Local/NosSmooth.Comms.LocalData/FocusMessage.cs +9 -0
@@ 0,0 1,9 @@
+//
+// FocusMessage.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Comms.LocalData;
+
+public record FocusMessage(long? EntityId);<
\ No newline at end of file
A src/Local/NosSmooth.Comms.LocalData/FollowMessage.cs => src/Local/NosSmooth.Comms.LocalData/FollowMessage.cs +9 -0
@@ 0,0 1,9 @@
+//
+// FollowMessage.cs
+//
+// Copyright (c) František Boháček. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace NosSmooth.Comms.LocalData;
+
+public record FollowMessage(long? EntityId);<
\ No newline at end of file
A src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj => src/Local/NosSmooth.Comms.LocalData/NosSmooth.Comms.LocalData.csproj +9 -0
@@ 0,0 1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net7.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+
+</Project>