~ruther/NosSmooth

9a95f09dc05dfddc03768589c001ee3547afeb25 — František Boháček 3 years ago 8610668
feat(core): add stateful entities support

Supports injecting entities depending on the current nostale client.
Useful for remote, pipe client etc.
M Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs => Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs +90 -6
@@ 8,10 8,12 @@ using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using NosSmooth.Core.Client;
using NosSmooth.Core.Commands;
using NosSmooth.Core.Commands.Control;
using NosSmooth.Core.Commands.Walking;
using NosSmooth.Core.Packets;
using NosSmooth.Core.Stateful;
using NosSmooth.Packets.Extensions;

namespace NosSmooth.Core.Extensions;


@@ 88,13 90,16 @@ public static class ServiceCollectionExtensions
            return serviceCollection.AddScoped(typeof(IEveryPacketResponder), responderType);
        }

        if (!responderType.GetInterfaces().Any(
        if (!responderType.GetInterfaces().Any
            (
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IPacketResponder<>)
            ))
        {
            throw new ArgumentException(
            throw new ArgumentException
            (
                $"{nameof(responderType)} should implement IPacketResponder.",
                nameof(responderType));
                nameof(responderType)
            );
        }

        var responderTypeInterfaces = responderType.GetInterfaces();


@@ 140,13 145,16 @@ public static class ServiceCollectionExtensions
        Type commandHandlerType
    )
    {
        if (!commandHandlerType.GetInterfaces().Any(
        if (!commandHandlerType.GetInterfaces().Any
            (
                i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandler<>)
            ))
        {
            throw new ArgumentException(
            throw new ArgumentException
            (
                $"{nameof(commandHandlerType)} should implement ICommandHandler.",
                nameof(commandHandlerType));
                nameof(commandHandlerType)
            );
        }

        var handlerTypeInterfaces = commandHandlerType.GetInterfaces();


@@ 186,4 194,80 @@ public static class ServiceCollectionExtensions
    {
        return serviceCollection.AddScoped<IPostExecutionEvent, TEvent>();
    }

    /// <summary>
    /// Add the injector for stateful entities that may be replaced for different NosTale clients.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddStatefulInjector(this IServiceCollection serviceCollection)
    {
        serviceCollection.RemoveAll<INostaleClient>();
        return serviceCollection
            .AddScoped<StatefulInjector>()
            .AddSingleton<StatefulRepository>()
            .AddPreExecutionEvent<StatefulPreExecutionEvent>()
            .AddScoped<INostaleClient>
            (
                p =>
                {
                    var nostaleClient = p.GetRequiredService<StatefulInjector>().Client;
                    if (nostaleClient == null)
                    {
                        throw new NullReferenceException("The client cannot be null.");
                    }

                    return nostaleClient;
                }
            )
            .ReplaceStatefulEntities();
    }

    /// <summary>
    /// Replace all the stateful entities that are added in the collection.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection ReplaceStatefulEntities(this IServiceCollection serviceCollection)
    {
        foreach (var serviceDescriptor in serviceCollection)
        {
            var type = serviceDescriptor.ServiceType;
            if (typeof(IStatefulEntity).IsAssignableFrom(type))
            {
                serviceCollection.AddStatefulEntity(type);
            }
        }

        return serviceCollection;
    }

    /// <summary>
    /// Add a stateful entity of the given type to be injectable into the scope of a client.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <typeparam name="TEntity">The type of the stateful entity.</typeparam>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddStatefulEntity<TEntity>(this IServiceCollection serviceCollection)
        => serviceCollection.AddStatefulEntity(typeof(TEntity));

    /// <summary>
    /// Add a stateful entity of the given type to be injectable into the scope of a client.
    /// </summary>
    /// <param name="serviceCollection">The service collection.</param>
    /// <param name="statefulEntityType">The type of the stateful entity.</param>
    /// <returns>The collection.</returns>
    public static IServiceCollection AddStatefulEntity
        (this IServiceCollection serviceCollection, Type statefulEntityType)
    {
        serviceCollection.AddStatefulInjector();
        serviceCollection.RemoveAll(statefulEntityType);
        serviceCollection.Add
        (
            ServiceDescriptor.Scoped
                (statefulEntityType, (p) => p.GetRequiredService<StatefulInjector>().GetEntity(p, statefulEntityType))
        );

        return serviceCollection;
    }
}
\ No newline at end of file

M Core/NosSmooth.Core/Packets/IPacketHandler.cs => Core/NosSmooth.Core/Packets/IPacketHandler.cs +5 -2
@@ 6,6 6,7 @@

using System.Threading;
using System.Threading.Tasks;
using NosSmooth.Core.Client;
using NosSmooth.Packets;
using Remora.Results;



@@ 19,18 20,20 @@ public interface IPacketHandler
    /// <summary>
    /// Calls a responder for the given packet.
    /// </summary>
    /// <param name="client">The current NosTale client.</param>
    /// <param name="packet">The packet.</param>
    /// <param name="packetString">The string of the packet.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> HandleReceivedPacketAsync(IPacket packet, string packetString, CancellationToken ct = default);
    public Task<Result> HandleReceivedPacketAsync(INostaleClient client, IPacket packet, string packetString, CancellationToken ct = default);

    /// <summary>
    /// Calls a responder for the given packet.
    /// </summary>
    /// <param name="client">The current NosTale client.</param>
    /// <param name="packet">The packet.</param>
    /// <param name="packetString">The string of the packet.</param>
    /// <param name="ct">The cancellation token for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    public Task<Result> HandleSentPacketAsync(IPacket packet, string packetString, CancellationToken ct = default);
    public Task<Result> HandleSentPacketAsync(INostaleClient client, IPacket packet, string packetString, CancellationToken ct = default);
}

A Core/NosSmooth.Core/Stateful/IStatefulEntity.cs => Core/NosSmooth.Core/Stateful/IStatefulEntity.cs +21 -0
@@ 0,0 1,21 @@
//
//  IStatefulEntity.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.Core.Client;

namespace NosSmooth.Core.Stateful;

/// <summary>
/// An entity that has a state that depends on the NosTale state.
/// </summary>
/// <remarks>
/// Stateful entities can be replaced with scoped entities that will be
/// injected depending on the current <see cref="INostaleClient"/>.
/// That can be useful in an application that has multiple nostale clients.
/// </remarks>
public interface IStatefulEntity
{
}
\ No newline at end of file

A Core/NosSmooth.Core/Stateful/StatefulInjector.cs => Core/NosSmooth.Core/Stateful/StatefulInjector.cs +49 -0
@@ 0,0 1,49 @@
//
//  StatefulInjector.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;
using NosSmooth.Core.Client;

namespace NosSmooth.Core.Stateful;

/// <summary>
/// The scoped injector of stateful entities.
/// </summary>
public class StatefulInjector
{
    private readonly StatefulRepository _repository;

    /// <summary>
    /// Initializes a new instance of the <see cref="StatefulInjector"/> class.
    /// </summary>
    /// <param name="repository">The repository of stateful entity types.</param>
    public StatefulInjector(StatefulRepository repository)
    {
        _repository = repository;
    }

    /// <summary>
    /// Gets or sets the nostale client.
    /// </summary>
    public INostaleClient? Client { get; set; }

    /// <summary>
    /// Gets an entity of the specified type.
    /// </summary>
    /// <param name="services">The service provider.</param>
    /// <param name="statefulEntityType">The type of the entity.</param>
    /// <exception cref="NullReferenceException">Thrown if the client is not set.</exception>
    /// <returns>The entity to inject.</returns>
    public object GetEntity(IServiceProvider services, Type statefulEntityType)
    {
        if (Client is null)
        {
            throw new NullReferenceException("The client cannot be null in order to get a stateful entity.");
        }

        return _repository.GetEntity(services, Client, statefulEntityType);
    }
}
\ No newline at end of file

A Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs => Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs +42 -0
@@ 0,0 1,42 @@
//
//  StatefulPreExecutionEvent.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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Core.Client;
using NosSmooth.Core.Packets;
using NosSmooth.Packets;
using Remora.Results;

namespace NosSmooth.Core.Stateful;

/// <summary>
/// Event that injects stateful entities into the scope.
/// </summary>
public class StatefulPreExecutionEvent : IPreExecutionEvent
{
    private readonly StatefulInjector _injector;

    /// <summary>
    /// Initializes a new instance of the <see cref="StatefulPreExecutionEvent"/> class.
    /// </summary>
    /// <param name="injector">The injector.</param>
    public StatefulPreExecutionEvent(StatefulInjector injector)
    {
        _injector = injector;
    }

    /// <inheritdoc />
    public Task<Result> ExecuteBeforeExecutionAsync<TPacket>
        (INostaleClient client, PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
        where TPacket : IPacket
    {
        _injector.Client = client;
        return Task.FromResult(Result.FromSuccess());
    }
}
\ No newline at end of file

A Core/NosSmooth.Core/Stateful/StatefulRepository.cs => Core/NosSmooth.Core/Stateful/StatefulRepository.cs +52 -0
@@ 0,0 1,52 @@
//
//  StatefulRepository.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;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using NosSmooth.Core.Client;

namespace NosSmooth.Core.Stateful;

/// <summary>
/// Repository holding all the stateful entities for various NosTale clients.
/// </summary>
public class StatefulRepository
{
    private readonly Dictionary<INostaleClient, Dictionary<Type, object>> _statefulEntities;

    /// <summary>
    /// Initializes a new instance of the <see cref="StatefulRepository"/> class.
    /// </summary>
    public StatefulRepository()
    {
        _statefulEntities = new Dictionary<INostaleClient, Dictionary<Type, object>>();
    }

    /// <summary>
    /// Get an entity for the given client and type.
    /// </summary>
    /// <param name="services">The service provider.</param>
    /// <param name="client">The NosTale client.</param>
    /// <param name="statefulEntityType">The type of the stateful entity to obtain.</param>
    /// <returns>The obtained entity.</returns>
    public object GetEntity(IServiceProvider services, INostaleClient client, Type statefulEntityType)
    {
        if (!_statefulEntities.ContainsKey(client))
        {
            _statefulEntities.Add(client, new Dictionary<Type, object>());
        }

        var value = _statefulEntities[client];
        if (!value.ContainsKey(statefulEntityType))
        {
            value.Add(statefulEntityType, ActivatorUtilities.CreateInstance(services, statefulEntityType));
        }

        return value[statefulEntityType];
    }
}
\ No newline at end of file

Do not follow this link