From a78ad7f37853322561d88a76b45208e95e96a861 Mon Sep 17 00:00:00 2001 From: Tides Date: Thu, 18 Jan 2024 15:41:16 -0500 Subject: [PATCH] Command refactor start --- .../_Attributes/CommandInfoAttribute.cs | 2 +- Obsidian.API/_Types/CommandContext.cs | 18 ++----- Obsidian/Commands/Builders/CommandBuilder.cs | 14 ++++-- Obsidian/Commands/CommandModuleFactory.cs | 23 +++++++++ .../Framework/CommandContextAttribute.cs | 6 +++ Obsidian/Commands/Framework/CommandHandler.cs | 47 ++++++++----------- .../Commands/Framework/CommandModuleBase.cs | 34 ++++++++++++++ .../Commands/Framework/Entities/Command.cs | 27 +++++++---- Obsidian/Plugins/PluginContainer.cs | 4 +- Obsidian/Plugins/PluginRegistry.cs | 22 ++++----- 10 files changed, 129 insertions(+), 68 deletions(-) create mode 100644 Obsidian/Commands/CommandModuleFactory.cs create mode 100644 Obsidian/Commands/Framework/CommandContextAttribute.cs create mode 100644 Obsidian/Commands/Framework/CommandModuleBase.cs diff --git a/Obsidian.API/_Attributes/CommandInfoAttribute.cs b/Obsidian.API/_Attributes/CommandInfoAttribute.cs index a10b35c85..52e3fcbb6 100644 --- a/Obsidian.API/_Attributes/CommandInfoAttribute.cs +++ b/Obsidian.API/_Attributes/CommandInfoAttribute.cs @@ -1,6 +1,6 @@ namespace Obsidian.API; -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)] +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = false)] public sealed class CommandInfoAttribute : Attribute { public string Description { get; } diff --git a/Obsidian.API/_Types/CommandContext.cs b/Obsidian.API/_Types/CommandContext.cs index afa26f3c7..3bee9e0bf 100644 --- a/Obsidian.API/_Types/CommandContext.cs +++ b/Obsidian.API/_Types/CommandContext.cs @@ -2,21 +2,13 @@ namespace Obsidian.API; -public sealed class CommandContext +public sealed class CommandContext(string message, ICommandSender commandSender, IPlayer player, IServer server) { - public IPlayer? Player { get; private set; } - public IServer Server { get; private set; } - public ICommandSender Sender { get; } + public IPlayer? Player { get; } = player; + public IServer Server { get; } = server; + public ICommandSender Sender { get; } = commandSender; public bool IsPlayer => Player != null; public PluginBase? Plugin { get; internal set; } - internal string Message { get; } - - public CommandContext(string message, ICommandSender commandSender, IPlayer player, IServer server) - { - Server = server; - Sender = commandSender; - Player = player; - Message = message; - } + internal string Message { get; } = message; } diff --git a/Obsidian/Commands/Builders/CommandBuilder.cs b/Obsidian/Commands/Builders/CommandBuilder.cs index 395fa229f..a3f7b7f41 100644 --- a/Obsidian/Commands/Builders/CommandBuilder.cs +++ b/Obsidian/Commands/Builders/CommandBuilder.cs @@ -1,4 +1,5 @@ -using Obsidian.API.BlockStates; +using Microsoft.Extensions.DependencyInjection; +using Obsidian.API.BlockStates; using Obsidian.Commands.Framework; using Obsidian.Commands.Framework.Entities; using Obsidian.Plugins; @@ -13,7 +14,7 @@ public sealed class CommandBuilder public string Name { get; } - public Type ModuleType { get; } + public Type? ModuleType { get; } public string? Description { get; private set; } = default!; @@ -33,8 +34,15 @@ private CommandBuilder(string name, Type moduleType) this.ModuleType = moduleType; } + private CommandBuilder(string name) + { + this.Name = name; + } + public static CommandBuilder Create(string name, Type moduleType) => new(name, moduleType); + public static CommandBuilder Create(string name) => new(name); + public CommandBuilder WithDescription(string? description) { this.Description = description; @@ -128,7 +136,6 @@ public CommandBuilder CanIssueAs(CommandIssuers issuers) return this; } - public Command Build(CommandHandler commandHandler, PluginContainer pluginContainer) => new() { Name = this.Name, @@ -142,5 +149,6 @@ public CommandBuilder CanIssueAs(CommandIssuers issuers) ModuleType = this.ModuleType, CommandHandler = commandHandler, PluginContainer = pluginContainer, + ModuleFactory = this.ModuleType != null ? ActivatorUtilities.CreateFactory(this.ModuleType, []) : null }; } diff --git a/Obsidian/Commands/CommandModuleFactory.cs b/Obsidian/Commands/CommandModuleFactory.cs new file mode 100644 index 000000000..42017e242 --- /dev/null +++ b/Obsidian/Commands/CommandModuleFactory.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using Obsidian.Commands.Framework; +using Obsidian.Plugins; +using System.Reflection; + +namespace Obsidian.Commands; +public static class CommandModuleFactory +{ + public static object? CreateModule(ObjectFactory factory, CommandContext context, PluginContainer pluginContainer) + { + var module = factory.Invoke(pluginContainer.ServiceScope.ServiceProvider, null); + var moduleType = module.GetType(); + + var commandContextProperty = moduleType.GetProperties().FirstOrDefault(x => x.GetCustomAttribute() != null) + ?? throw new InvalidOperationException("Failed to find CommandContext property."); + + commandContextProperty.SetValue(module, context); + + pluginContainer.InjectServices(null, module); + + return module; + } +} diff --git a/Obsidian/Commands/Framework/CommandContextAttribute.cs b/Obsidian/Commands/Framework/CommandContextAttribute.cs new file mode 100644 index 000000000..4c66fbfa1 --- /dev/null +++ b/Obsidian/Commands/Framework/CommandContextAttribute.cs @@ -0,0 +1,6 @@ +namespace Obsidian.Commands.Framework; + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public sealed class CommandContextAttribute : Attribute +{ +} diff --git a/Obsidian/Commands/Framework/CommandHandler.cs b/Obsidian/Commands/Framework/CommandHandler.cs index 7155451c8..fb3c6257b 100644 --- a/Obsidian/Commands/Framework/CommandHandler.cs +++ b/Obsidian/Commands/Framework/CommandHandler.cs @@ -12,7 +12,7 @@ namespace Obsidian.Commands.Framework; public sealed class CommandHandler { - private readonly ILogger? logger; + internal readonly ILogger? logger; private readonly List _commands; private readonly CommandParser _commandParser; private readonly List _argumentParsers; @@ -55,6 +55,25 @@ public BaseArgumentParser GetArgumentParser(Type argumentType) => public Command[] GetAllCommands() => _commands.ToArray(); + public void RegisterCommand(PluginContainer pluginContainer, string name, Delegate commandDelegate) + { + var method = commandDelegate.Method; + + var commandInfo = method.GetCustomAttribute(); + var checks = method.GetCustomAttributes(); + var issuers = method.GetCustomAttribute()?.Issuers ?? CommandHelpers.DefaultIssuerScope; + + var command = CommandBuilder.Create(name) + .WithDescription(commandInfo?.Description) + .WithUsage(commandInfo?.Usage) + .AddExecutionChecks(checks) + .CanIssueAs(issuers) + .AddOverload(method) + .Build(this, pluginContainer); + + _commands.Add(command); + } + public void AddArgumentParser(BaseArgumentParser parser) => _argumentParsers.Add(parser); public void UnregisterPluginCommands(PluginContainer plugin) => _commands.RemoveAll(x => x.PluginContainer == plugin); @@ -81,32 +100,6 @@ public void RegisterCommands(PluginContainer pluginContainer) } } - public object? CreateCommandRootInstance(Type moduleType, PluginContainer pluginContainer) - { - ArgumentNullException.ThrowIfNull(moduleType); - - object? instance = Activator.CreateInstance(moduleType); - if (instance is null) - return null; - - var injectables = moduleType.GetProperties().Where(x => x.GetCustomAttribute() != null); - foreach (var injectable in injectables) - { - //Plugins should stick to services and not be able to have access to other plugin base class. - //if (injectable.PropertyType == typeof(PluginBase) || injectable.PropertyType == plugin.Plugin.GetType()) - //{ - // injectable.SetValue(instance, plugin.Plugin); - //} - //else - //{ - //} - - pluginContainer.InjectServices(this.logger!, instance); - } - - return instance; - } - private void RegisterSubgroups(Type moduleType, PluginContainer pluginContainer, Command? parent = null) { // find all command groups under this command diff --git a/Obsidian/Commands/Framework/CommandModuleBase.cs b/Obsidian/Commands/Framework/CommandModuleBase.cs new file mode 100644 index 000000000..b50f6db86 --- /dev/null +++ b/Obsidian/Commands/Framework/CommandModuleBase.cs @@ -0,0 +1,34 @@ +using Obsidian.API.Plugins; +using System.Diagnostics; + +namespace Obsidian.Commands.Framework; +public abstract class CommandModuleBase +{ + private CommandContext? commandContext; + + [CommandContext] + public CommandContext CommandContext + { + get + { + if (commandContext == null) + throw new UnreachableException();//TODO empty command context maybe?? + + return this.commandContext; + } + set + { + ArgumentNullException.ThrowIfNull(value); + + this.commandContext = value; + } + } + + public IPlayer? Player => this.CommandContext.Player; + public IServer Server => this.CommandContext.Server; + public ICommandSender Sender => this.CommandContext.Sender; + + public bool IsPlayer => this.CommandContext.IsPlayer; + + public PluginBase? Plugin => this.CommandContext.Plugin; +} diff --git a/Obsidian/Commands/Framework/Entities/Command.cs b/Obsidian/Commands/Framework/Entities/Command.cs index 399fd6323..207a58857 100644 --- a/Obsidian/Commands/Framework/Entities/Command.cs +++ b/Obsidian/Commands/Framework/Entities/Command.cs @@ -1,6 +1,8 @@ -using Obsidian.API.Utilities; +using Microsoft.Extensions.DependencyInjection; +using Obsidian.API.Utilities; using Obsidian.Commands.Framework.Exceptions; using Obsidian.Plugins; +using System; using System.Reflection; namespace Obsidian.Commands.Framework.Entities; @@ -9,7 +11,7 @@ public sealed class Command { internal CommandIssuers AllowedIssuers { get; init; } - public required Type ModuleType { get; init; } + public required Type? ModuleType { get; init; } public required CommandHandler CommandHandler { get; init; } public required PluginContainer PluginContainer { get; init; } @@ -24,6 +26,8 @@ public sealed class Command public Command? Parent { get; init; } + public ObjectFactory? ModuleFactory { get; init; } + internal Command() { } public bool CheckCommand(string[] input, Command? parent) @@ -78,14 +82,24 @@ public async Task ExecuteAsync(CommandContext context, string[] args) || x.GetParameters().Last().GetCustomAttribute() != null); // Create instance of declaring type to execute. - var module = CommandHandler.CreateCommandRootInstance(ModuleType, PluginContainer); + + if(this.ModuleType != null) + { + await this.ExecuteFromModuleAsync(method, context, args); + return; + } + + } + + private async Task ExecuteFromModuleAsync(MethodInfo method, CommandContext context, string[] args) + { + var module = CommandModuleFactory.CreateModule(this.ModuleFactory!, context, this.PluginContainer); // Get required params var methodparams = method.GetParameters().Skip(1).ToArray(); // Set first parameter to be the context. var parsedargs = new object[methodparams.Length + 1]; - parsedargs[0] = context; // TODO comments for (int i = 0; i < methodparams.Length; i++) @@ -105,14 +119,11 @@ public async Task ExecuteAsync(CommandContext context, string[] args) { var parser = CommandHandler.GetArgumentParser(paraminfo.ParameterType); - // sets args for parser method - var parseargs = new object?[3] { arg, context, null }; - // cast with reflection? if (parser.TryParseArgument(arg, context, out var parserResult)) { // parse success! - parsedargs[i + 1] = parserResult; + parsedargs[i] = parserResult; } else { diff --git a/Obsidian/Plugins/PluginContainer.cs b/Obsidian/Plugins/PluginContainer.cs index e27fd16b9..9dfbba14b 100644 --- a/Obsidian/Plugins/PluginContainer.cs +++ b/Obsidian/Plugins/PluginContainer.cs @@ -51,7 +51,7 @@ public PluginContainer(PluginBase plugin, PluginInfo info, Assembly assembly, As /// /// Inject the scoped services into /// - public void InjectServices(ILogger logger, object? target = null) + public void InjectServices(ILogger? logger, object? target = null) { var properties = target is null ? this.pluginType!.WithInjectAttribute() : target.GetType().WithInjectAttribute(); @@ -67,7 +67,7 @@ public void InjectServices(ILogger logger, object? target = null) } catch (Exception ex) { - logger.LogError(ex, "Failed to inject service."); + logger?.LogError(ex, "Failed to inject service."); } } diff --git a/Obsidian/Plugins/PluginRegistry.cs b/Obsidian/Plugins/PluginRegistry.cs index 3f5dfc3b6..4066a4e6f 100644 --- a/Obsidian/Plugins/PluginRegistry.cs +++ b/Obsidian/Plugins/PluginRegistry.cs @@ -12,50 +12,44 @@ public sealed class PluginRegistry(PluginManager pluginManager, EventDispatcher private readonly CommandHandler commandHandler = commandHandler; private readonly ILogger logger = logger; - //TODO REGISTER DELEGATES + public PluginContainer PluginContainer => this.pluginManager.GetPluginContainerByAssembly(); + public IPluginRegistry MapCommand(string name, Delegate handler) { - + this.commandHandler.RegisterCommand(this.PluginContainer, name, handler); return this; } - //TODO REGISTER DELEGATES public IPluginRegistry MapCommand(string name, ValueTaskContextDelegate contextDelegate) { - + this.commandHandler.RegisterCommand(this.PluginContainer, name, contextDelegate); return this; } public IPluginRegistry MapCommands() { - this.commandHandler.RegisterCommands(this.pluginManager.GetPluginContainerByAssembly()); + this.commandHandler.RegisterCommands(this.PluginContainer); return this; } public IPluginRegistry MapEvent(ValueTaskContextDelegate contextDelegate, Priority priority = Priority.Low) where TEventArgs : BaseMinecraftEventArgs { - var pluginContainer = this.pluginManager.GetPluginContainerByAssembly(); - - this.eventDispatcher.RegisterEvent(pluginContainer, contextDelegate, priority); + this.eventDispatcher.RegisterEvent(this.PluginContainer, contextDelegate, priority); return this; } public IPluginRegistry MapEvent(Delegate handler, Priority priority = Priority.Low) { - var pluginContainer = this.pluginManager.GetPluginContainerByAssembly(); - - this.eventDispatcher.RegisterEvent(pluginContainer, handler, priority); + this.eventDispatcher.RegisterEvent(this.PluginContainer, handler, priority); return this; } public IPluginRegistry MapEvents() { - var pluginContainer = this.pluginManager.GetPluginContainerByAssembly(); - - this.eventDispatcher.RegisterEvents(pluginContainer); + this.eventDispatcher.RegisterEvents(this.PluginContainer); return this; }