From 9a95f09dc05dfddc03768589c001ee3547afeb25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Boh=C3=A1=C4=8Dek?= Date: Sun, 13 Feb 2022 15:36:25 +0100 Subject: [PATCH] feat(core): add stateful entities support Supports injecting entities depending on the current nostale client. Useful for remote, pipe client etc. --- .../Extensions/ServiceCollectionExtensions.cs | 96 +++++++++++++++++-- Core/NosSmooth.Core/Packets/IPacketHandler.cs | 7 +- .../Stateful/IStatefulEntity.cs | 21 ++++ .../Stateful/StatefulInjector.cs | 49 ++++++++++ .../Stateful/StatefulPreExecutionEvent.cs | 42 ++++++++ .../Stateful/StatefulRepository.cs | 52 ++++++++++ 6 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 Core/NosSmooth.Core/Stateful/IStatefulEntity.cs create mode 100644 Core/NosSmooth.Core/Stateful/StatefulInjector.cs create mode 100644 Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs create mode 100644 Core/NosSmooth.Core/Stateful/StatefulRepository.cs diff --git a/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs b/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs index 611646b..67cfaa4 100644 --- a/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs +++ b/Core/NosSmooth.Core/Extensions/ServiceCollectionExtensions.cs @@ -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(); } + + /// + /// Add the injector for stateful entities that may be replaced for different NosTale clients. + /// + /// The service collection. + /// The collection. + public static IServiceCollection AddStatefulInjector(this IServiceCollection serviceCollection) + { + serviceCollection.RemoveAll(); + return serviceCollection + .AddScoped() + .AddSingleton() + .AddPreExecutionEvent() + .AddScoped + ( + p => + { + var nostaleClient = p.GetRequiredService().Client; + if (nostaleClient == null) + { + throw new NullReferenceException("The client cannot be null."); + } + + return nostaleClient; + } + ) + .ReplaceStatefulEntities(); + } + + /// + /// Replace all the stateful entities that are added in the collection. + /// + /// The service collection. + /// The collection. + 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; + } + + /// + /// Add a stateful entity of the given type to be injectable into the scope of a client. + /// + /// The service collection. + /// The type of the stateful entity. + /// The collection. + public static IServiceCollection AddStatefulEntity(this IServiceCollection serviceCollection) + => serviceCollection.AddStatefulEntity(typeof(TEntity)); + + /// + /// Add a stateful entity of the given type to be injectable into the scope of a client. + /// + /// The service collection. + /// The type of the stateful entity. + /// The collection. + public static IServiceCollection AddStatefulEntity + (this IServiceCollection serviceCollection, Type statefulEntityType) + { + serviceCollection.AddStatefulInjector(); + serviceCollection.RemoveAll(statefulEntityType); + serviceCollection.Add + ( + ServiceDescriptor.Scoped + (statefulEntityType, (p) => p.GetRequiredService().GetEntity(p, statefulEntityType)) + ); + + return serviceCollection; + } } \ No newline at end of file diff --git a/Core/NosSmooth.Core/Packets/IPacketHandler.cs b/Core/NosSmooth.Core/Packets/IPacketHandler.cs index 5c689b9..424757a 100644 --- a/Core/NosSmooth.Core/Packets/IPacketHandler.cs +++ b/Core/NosSmooth.Core/Packets/IPacketHandler.cs @@ -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 /// /// Calls a responder for the given packet. /// + /// The current NosTale client. /// The packet. /// The string of the packet. /// The cancellation token for cancelling the operation. /// A result that may or may not have succeeded. - public Task HandleReceivedPacketAsync(IPacket packet, string packetString, CancellationToken ct = default); + public Task HandleReceivedPacketAsync(INostaleClient client, IPacket packet, string packetString, CancellationToken ct = default); /// /// Calls a responder for the given packet. /// + /// The current NosTale client. /// The packet. /// The string of the packet. /// The cancellation token for cancelling the operation. /// A result that may or may not have succeeded. - public Task HandleSentPacketAsync(IPacket packet, string packetString, CancellationToken ct = default); + public Task HandleSentPacketAsync(INostaleClient client, IPacket packet, string packetString, CancellationToken ct = default); } diff --git a/Core/NosSmooth.Core/Stateful/IStatefulEntity.cs b/Core/NosSmooth.Core/Stateful/IStatefulEntity.cs new file mode 100644 index 0000000..501ab08 --- /dev/null +++ b/Core/NosSmooth.Core/Stateful/IStatefulEntity.cs @@ -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; + +/// +/// An entity that has a state that depends on the NosTale state. +/// +/// +/// Stateful entities can be replaced with scoped entities that will be +/// injected depending on the current . +/// That can be useful in an application that has multiple nostale clients. +/// +public interface IStatefulEntity +{ +} \ No newline at end of file diff --git a/Core/NosSmooth.Core/Stateful/StatefulInjector.cs b/Core/NosSmooth.Core/Stateful/StatefulInjector.cs new file mode 100644 index 0000000..4f81355 --- /dev/null +++ b/Core/NosSmooth.Core/Stateful/StatefulInjector.cs @@ -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; + +/// +/// The scoped injector of stateful entities. +/// +public class StatefulInjector +{ + private readonly StatefulRepository _repository; + + /// + /// Initializes a new instance of the class. + /// + /// The repository of stateful entity types. + public StatefulInjector(StatefulRepository repository) + { + _repository = repository; + } + + /// + /// Gets or sets the nostale client. + /// + public INostaleClient? Client { get; set; } + + /// + /// Gets an entity of the specified type. + /// + /// The service provider. + /// The type of the entity. + /// Thrown if the client is not set. + /// The entity to inject. + 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 diff --git a/Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs b/Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs new file mode 100644 index 0000000..0771abc --- /dev/null +++ b/Core/NosSmooth.Core/Stateful/StatefulPreExecutionEvent.cs @@ -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; + +/// +/// Event that injects stateful entities into the scope. +/// +public class StatefulPreExecutionEvent : IPreExecutionEvent +{ + private readonly StatefulInjector _injector; + + /// + /// Initializes a new instance of the class. + /// + /// The injector. + public StatefulPreExecutionEvent(StatefulInjector injector) + { + _injector = injector; + } + + /// + public Task ExecuteBeforeExecutionAsync + (INostaleClient client, PacketEventArgs packetArgs, CancellationToken ct = default) + where TPacket : IPacket + { + _injector.Client = client; + return Task.FromResult(Result.FromSuccess()); + } +} \ No newline at end of file diff --git a/Core/NosSmooth.Core/Stateful/StatefulRepository.cs b/Core/NosSmooth.Core/Stateful/StatefulRepository.cs new file mode 100644 index 0000000..e4f215b --- /dev/null +++ b/Core/NosSmooth.Core/Stateful/StatefulRepository.cs @@ -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; + +/// +/// Repository holding all the stateful entities for various NosTale clients. +/// +public class StatefulRepository +{ + private readonly Dictionary> _statefulEntities; + + /// + /// Initializes a new instance of the class. + /// + public StatefulRepository() + { + _statefulEntities = new Dictionary>(); + } + + /// + /// Get an entity for the given client and type. + /// + /// The service provider. + /// The NosTale client. + /// The type of the stateful entity to obtain. + /// The obtained entity. + public object GetEntity(IServiceProvider services, INostaleClient client, Type statefulEntityType) + { + if (!_statefulEntities.ContainsKey(client)) + { + _statefulEntities.Add(client, new Dictionary()); + } + + var value = _statefulEntities[client]; + if (!value.ContainsKey(statefulEntityType)) + { + value.Add(statefulEntityType, ActivatorUtilities.CreateInstance(services, statefulEntityType)); + } + + return value[statefulEntityType]; + } +} \ No newline at end of file -- 2.49.0