diff --git a/VCF.Core/Basics/BasicAdminCheck.cs b/VCF.Core/Basics/BasicAdminCheck.cs index fd24959..7ac887b 100644 --- a/VCF.Core/Basics/BasicAdminCheck.cs +++ b/VCF.Core/Basics/BasicAdminCheck.cs @@ -8,7 +8,7 @@ public class BasicAdminCheck : CommandMiddleware { public override bool CanExecute(ICommandContext ctx, CommandAttribute cmd, MethodInfo m) { - Log.Debug($"Running BasicAdmin Check adminOnly: {cmd.AdminOnly} IsAdmin: {ctx.IsAdmin}"); + // Log.Debug($"Running BasicAdmin Check adminOnly: {cmd.AdminOnly} IsAdmin: {ctx.IsAdmin}"); return !cmd.AdminOnly || ctx.IsAdmin; } } diff --git a/VCF.Core/Basics/HelpCommand.cs b/VCF.Core/Basics/HelpCommand.cs index bfa793b..b4b02f2 100644 --- a/VCF.Core/Basics/HelpCommand.cs +++ b/VCF.Core/Basics/HelpCommand.cs @@ -41,7 +41,7 @@ public static void HelpCommand(ICommandContext ctx, string search = null) || x.Value.Contains(search, StringComparer.InvariantCultureIgnoreCase) ); - individualResults.Where(kvp => CommandRegistry.CanCommandExecute(ctx, kvp.Key)); + individualResults = individualResults.Where(kvp => CommandRegistry.CanCommandExecute(ctx, kvp.Key)); if (!individualResults.Any()) { @@ -51,7 +51,7 @@ public static void HelpCommand(ICommandContext ctx, string search = null) var sb = new StringBuilder(); foreach (var command in individualResults) { - PrintCommandHelp(command.Key, command.Value, sb); + GenerateFullHelp(command.Key, command.Value, sb); } ctx.SysPaginatedReply(sb); @@ -67,7 +67,7 @@ public static void HelpCommand(ICommandContext ctx, string search = null) } ctx.SysPaginatedReply(sb); } - + void PrintAssemblyHelp(ICommandContext ctx, KeyValuePair>> assembly, StringBuilder sb) { var name = assembly.Key.GetName().Name; @@ -78,20 +78,37 @@ void PrintAssemblyHelp(ICommandContext ctx, KeyValuePair aliases, StringBuilder sb) + void GenerateFullHelp(CommandMetadata command, List aliases, StringBuilder sb) { - sb.AppendLine($"{B(command.Attribute.Name)} ({command.Attribute.Id}) {command.Attribute.Description}"); - sb.AppendLine(GenerateHelpText(command)); - sb.AppendLine($"Aliases: {string.Join(", ", aliases).Italic()}"); + sb.AppendLine(PrintShortHelp(command)); + sb.AppendLine($"{B("Aliases").Underline()}: {string.Join(", ", aliases).Italic()}"); + + // Automatically Display Enum types + var enums = command.Parameters.Select(p => p.ParameterType).Distinct().Where(t => t.IsEnum); + foreach (var e in enums) + { + sb.AppendLine($"{Format.Bold($"{e.Name} Values").Underline()}: {string.Join(", ", Enum.GetNames(e))}"); + } + + // Check CommandRegistry for types that can be converted and further for IConverterUsage + var converters = command.Parameters.Select(p => p.ParameterType).Distinct().Where(p => CommandRegistry._converters.ContainsKey(p)); + foreach (var c in converters) + { + var (obj, _, _) = CommandRegistry._converters[c]; + if (obj is not IConverterUsage) continue; + IConverterUsage converterUsage = obj as IConverterUsage; + + sb.AppendLine($"{Format.Bold($"{c.Name}")}: {converterUsage.Usage}"); + } } } - internal static string GenerateHelpText(CommandMetadata command) + internal static string PrintShortHelp(CommandMetadata command) { var attr = command.Attribute; var groupPrefix = string.IsNullOrEmpty(command.GroupAttribute?.Name) ? string.Empty : $"{command.GroupAttribute.Name} "; diff --git a/VCF.Core/Common/Utility.cs b/VCF.Core/Common/Utility.cs index 15fd2cc..35f5f85 100644 --- a/VCF.Core/Common/Utility.cs +++ b/VCF.Core/Common/Utility.cs @@ -1,6 +1,7 @@ using RootMotion.FinalIK; using System; using System.Collections.Generic; +using System.Linq; using System.Text; namespace VampireCommandFramework.Common; @@ -34,7 +35,7 @@ internal static IEnumerable GetParts(string input) } } - internal static void InternalError(this ICommandContext ctx) => ctx.SysReply("An internal error has occured."); + internal static void InternalError(this ICommandContext ctx) => ctx.SysReply("An internal error has occurred."); internal static void SysReply(this ICommandContext ctx, string input) => ctx.Reply($"[vcf] ".Color(Color.Primary) + input.Color(Color.White)); @@ -67,10 +68,40 @@ internal static string[] SplitIntoPages(string rawText, int pageSize = MAX_MESSA { var pages = new List(); var page = new StringBuilder(); - var lines = rawText.Split(Environment.NewLine); // todo: does this work on both platofrms? + var rawLines = rawText.Split(Environment.NewLine); // todo: does this work on both platofrms? + var lines = new List(); + + // process rawLines -> lines of length <= pageSize + foreach (var line in rawLines) + { + if (line.Length > pageSize) + { + // split into lines of max size preferring to split on spaces + var remaining = line; + while (!string.IsNullOrWhiteSpace(remaining) && remaining.Length > pageSize) + { + // find the last space before the page size within 5% of pageSize buffer + var splitIndex = remaining.LastIndexOf(' ', pageSize - (int)(pageSize * 0.05)); + if (splitIndex < 0) + { + splitIndex = Math.Min(pageSize - 1, remaining.Length); + } + + lines.Add(remaining.Substring(0, splitIndex)); + remaining = remaining.Substring(splitIndex); + } + lines.Add(remaining); + } + else + { + lines.Add(line); + } + } + + // batch as many lines together into pageSize foreach (var line in lines) { - if (page.Length + line.Length > pageSize) + if ((page.Length + line.Length) > pageSize) { pages.Add(page.ToString()); page.Clear(); diff --git a/VCF.Core/Framework/ChatCommandContext.cs b/VCF.Core/Framework/ChatCommandContext.cs index 34a7487..7fa85b9 100644 --- a/VCF.Core/Framework/ChatCommandContext.cs +++ b/VCF.Core/Framework/ChatCommandContext.cs @@ -50,4 +50,4 @@ public CommandException Error(string LogMessage) { return new CommandException(LogMessage); } -} +} \ No newline at end of file diff --git a/VCF.Core/Framework/CommandArgumentConverter.cs b/VCF.Core/Framework/CommandArgumentConverter.cs index 1f23829..155b36d 100644 --- a/VCF.Core/Framework/CommandArgumentConverter.cs +++ b/VCF.Core/Framework/CommandArgumentConverter.cs @@ -7,4 +7,4 @@ public abstract class CommandArgumentConverter : CommandArgumentConverter where C : ICommandContext { public abstract T Parse(C ctx, string input); -} +} \ No newline at end of file diff --git a/VCF.Core/Framework/IConverterUsage.cs b/VCF.Core/Framework/IConverterUsage.cs new file mode 100644 index 0000000..823c411 --- /dev/null +++ b/VCF.Core/Framework/IConverterUsage.cs @@ -0,0 +1,13 @@ +namespace VampireCommandFramework; + +public interface IConverterUsage +{ + /// + /// Returns a description of the type that this converter can parse. This is used + /// in generated help messages. + /// + /// + /// You are expected to cache this data / make static. This property should be fast to retrieve. + /// + public string Usage { get; } +} \ No newline at end of file diff --git a/VCF.Core/Registry/CommandRegistry.cs b/VCF.Core/Registry/CommandRegistry.cs index 05961e8..80acec0 100644 --- a/VCF.Core/Registry/CommandRegistry.cs +++ b/VCF.Core/Registry/CommandRegistry.cs @@ -12,7 +12,10 @@ public static class CommandRegistry { internal const string DEFAULT_PREFIX = "."; private static CommandCache _cache = new(); - private static Dictionary _converters = new(); + /// + /// From converting type to (object instance, MethodInfo tryParse, Type contextType) + /// + internal static Dictionary _converters = new(); internal static void Reset() { @@ -30,10 +33,10 @@ internal static void Reset() internal static bool CanCommandExecute(ICommandContext ctx, CommandMetadata command) { - Log.Debug($"Executing {Middlewares.Count} CanHandle Middlwares:"); + // Log.Debug($"Executing {Middlewares.Count} CanHandle Middlwares:"); foreach (var middleware in Middlewares) { - Log.Debug($"\t{middleware.GetType().Name}"); + // Log.Debug($"\t{middleware.GetType().Name}"); try { if (!middleware.CanExecute(ctx, command.Attribute, command.Method)) @@ -76,7 +79,7 @@ static void HandleAfterExecute(ICommandContext ctx, CommandMetadata command) foreach (var possible in matchedCommand.PartialMatches) { - ctx.SysReply(Basics.HelpCommands.GenerateHelpText(possible)); + ctx.SysReply(Basics.HelpCommands.PrintShortHelp(possible)); } return CommandResult.UsageError; @@ -123,6 +126,7 @@ static void HandleAfterExecute(ICommandContext ctx, CommandMetadata command) var param = command.Parameters[i]; var arg = args[i]; + // Custom Converter if (_converters.TryGetValue(param.ParameterType, out var customConverter)) { var (converter, convertMethod, converterContextType) = customConverter; @@ -242,13 +246,13 @@ static void HandleAfterExecute(ICommandContext ctx, CommandMetadata command) } HandleAfterExecute(ctx, command); - + return CommandResult.Success; } public static void UnregisterConverter(Type converter) { - if(!IsGenericConverterContext(converter) && !IsSpecificConverterContext(converter)) + if (!IsGenericConverterContext(converter) && !IsSpecificConverterContext(converter)) { return; } @@ -260,7 +264,7 @@ public static void UnregisterConverter(Type converter) Log.Warning($"Could not resolve converter type {converter.Name}"); return; } - + if (_converters.ContainsKey(convertFrom)) { _converters.Remove(convertFrom); @@ -272,8 +276,8 @@ public static void UnregisterConverter(Type converter) } } - private static bool IsGenericConverterContext(Type rootType) => rootType.BaseType.Name == typeof(CommandArgumentConverter<>).Name; - private static bool IsSpecificConverterContext(Type rootType) => rootType.BaseType.Name == typeof(CommandArgumentConverter<,>).Name; + internal static bool IsGenericConverterContext(Type rootType) => rootType.BaseType.Name == typeof(CommandArgumentConverter<>).Name; + internal static bool IsSpecificConverterContext(Type rootType) => rootType.BaseType.Name == typeof(CommandArgumentConverter<,>).Name; public static void RegisterConverter(Type converter) { diff --git a/VCF.Core/VCF.Core.csproj b/VCF.Core/VCF.Core.csproj index 904bde9..46ec883 100644 --- a/VCF.Core/VCF.Core.csproj +++ b/VCF.Core/VCF.Core.csproj @@ -3,7 +3,7 @@ net6.0 VampireCommandFramework - My first plugin + Framework for commands in V Rising 0.0.999 true latest @@ -13,14 +13,15 @@ VampireCommandFramework VampireCommandFramework deca + preview - - + + diff --git a/VCF.Tests/AssertReplyContext.cs b/VCF.Tests/AssertReplyContext.cs index ef6e775..4eb945e 100644 --- a/VCF.Tests/AssertReplyContext.cs +++ b/VCF.Tests/AssertReplyContext.cs @@ -27,9 +27,14 @@ public void AssertReply(string expected) { Assert.That(_sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()), Is.EqualTo(expected)); } + public void AssertReplyContains(string expected) + { + var repliedText = _sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()); + Assert.That(repliedText.Contains(expected), Is.True, $"Expected {expected} to be contained in replied: {repliedText}"); + } public void AssertInternalError() { - Assert.That(_sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()), Is.EqualTo("[vcf] An internal error has occured.")); + Assert.That(_sb.ToString().TrimEnd(Environment.NewLine.ToCharArray()), Is.EqualTo("[vcf] An internal error has occurred.")); } } diff --git a/VCF.Tests/CommandArgumentConverterTests.cs b/VCF.Tests/CommandArgumentConverterTests.cs index e24a339..7dc0827 100644 --- a/VCF.Tests/CommandArgumentConverterTests.cs +++ b/VCF.Tests/CommandArgumentConverterTests.cs @@ -1,15 +1,16 @@ using FakeItEasy; using NUnit.Framework; using VampireCommandFramework; +using VampireCommandFramework.Basics; namespace VCF.Tests; public class CommandArgumentConverterTests { - public record SomeType() { readonly string Unique = Any.String(); }; + record SomeType() { readonly string Unique = Any.String(); }; - public static readonly SomeType ReturnedFromGeneric = new(), ReturnedFromSpecific = new(), DefaultValue = new(); + static readonly SomeType ReturnedFromGeneric = new(), ReturnedFromSpecific = new(), DefaultValue = new(); - public class GenericContextConverter : CommandArgumentConverter + class GenericContextConverter : CommandArgumentConverter { public override SomeType Parse(ICommandContext ctx, string input) { @@ -17,7 +18,7 @@ public override SomeType Parse(ICommandContext ctx, string input) } } - public class SpecificContextConverter : CommandArgumentConverter + class SpecificContextConverter : CommandArgumentConverter { public override SomeType Parse(SecondaryContext ctx, string input) { @@ -25,7 +26,7 @@ public override SomeType Parse(SecondaryContext ctx, string input) } } - public class GenericContextTestCommands + class GenericContextTestCommands { [Command("test")] public void TestCommand(ICommandContext ctx, SomeType value) { } @@ -34,7 +35,7 @@ public void TestCommand(ICommandContext ctx, SomeType value) { } public void TestWithefault(ICommandContext ctx, SomeType value = null) { } } - public class SecondaryContext : ICommandContext + internal class SecondaryContext : ICommandContext { public IServiceProvider Services => throw new NotImplementedException(); @@ -103,7 +104,7 @@ public void UnregisterConverter_RemovesConverter() { CommandRegistry.RegisterConverter(typeof(GenericContextConverter)); CommandRegistry.RegisterCommandType(typeof(GenericContextTestCommands)); - + var ctx = new SecondaryContext(); Assert.That(CommandRegistry.Handle(ctx, ".test something"), Is.EqualTo(CommandResult.Success)); @@ -163,5 +164,4 @@ public void CanConvert_SpecificContext_WithDefault() Assert.That(CommandRegistry.Handle(ctx, ".test-default"), Is.EqualTo(CommandResult.Success)); Assert.That(CommandRegistry.Handle(ctx, ".test-default something"), Is.EqualTo(CommandResult.Success)); } - } diff --git a/VCF.Tests/HelpTests.cs b/VCF.Tests/HelpTests.cs index 4831ef7..35a7e19 100644 --- a/VCF.Tests/HelpTests.cs +++ b/VCF.Tests/HelpTests.cs @@ -15,6 +15,27 @@ public class HelpTests { private AssertReplyContext AnyCtx; + record SomeType(); + + static readonly SomeType returnedFromConverter = new(); + + class SomeTypeConverter : CommandArgumentConverter, IConverterUsage + { + public string Usage => "TEST-SENTINEL"; + public override SomeType Parse(ICommandContext ctx, string input) => returnedFromConverter; + } + + enum SomeEnum { A, B, C } + + class HelpTestCommands + { + [Command("test-help")] + public void TestHelp(ICommandContext ctx, SomeEnum someEnum, SomeType? someType = null) + { + + } + } + [SetUp] public void Setup() { @@ -65,13 +86,30 @@ public void HelpCommand_Help_ShowSpecificCommand() """); } + [Test] + public void HelpCommand_Help_ListAll_IncludesNewCommands() + { + CommandRegistry.RegisterConverter(typeof(SomeTypeConverter)); + CommandRegistry.RegisterCommandType(typeof(HelpTestCommands)); + + Assert.That(CommandRegistry.Handle(AnyCtx, ".help"), Is.EqualTo(CommandResult.Success)); + AnyCtx.AssertReply($""" + [vcf] Listing all commands + Commands from VampireCommandFramework: + .help-legacy [search=] + .help [search=] + Commands from VCF.Tests: + .test-help (someEnum) [someType=] + """); + } + [Test] public void GenerateHelpText_UsageSpecified() { var (commandName, usage, description) = Any.ThreeStrings(); var command = new CommandMetadata(new CommandAttribute(commandName, usage: usage, description: description), null, null, null, null, null, null); - var text = HelpCommands.GenerateHelpText(command); + var text = HelpCommands.PrintShortHelp(command); Assert.That(text, Is.EqualTo($".{commandName} {usage}")); } @@ -85,7 +123,7 @@ public void GenerateHelpText_GeneratesUsage_NormalParam() var command = new CommandMetadata(new CommandAttribute(commandName, usage: null, description: description), null, null, new[] { param }, null, null, null); - var text = HelpCommands.GenerateHelpText(command); + var text = HelpCommands.PrintShortHelp(command); Assert.That(text, Is.EqualTo($".{commandName} ({paramName})")); } @@ -103,8 +141,32 @@ public void GenerateHelpText_GeneratesUsage_DefaultParam() var command = new CommandMetadata(new CommandAttribute(commandName, usage: null, description: description), null, null, new[] { param }, null, null, null); - var text = HelpCommands.GenerateHelpText(command); + var text = HelpCommands.PrintShortHelp(command); Assert.That(text, Is.EqualTo($".{commandName} [{paramName}={paramValue}]")); } + + [Test] + public void FullHelp_Usage_Includes_IConverterUsage() + { + CommandRegistry.RegisterConverter(typeof(SomeTypeConverter)); + CommandRegistry.RegisterCommandType(typeof(HelpTestCommands)); + + var ctx = new AssertReplyContext(); + Format.Mode = Format.FormatMode.None; + Assert.That(CommandRegistry.Handle(ctx, ".help test-help"), Is.EqualTo(CommandResult.Success)); + ctx.AssertReplyContains("SomeType: TEST-SENTINEL"); + } + + [Test] + public void FullHelp_Usage_Includes_Enum_Values() + { + CommandRegistry.RegisterConverter(typeof(SomeTypeConverter)); + CommandRegistry.RegisterCommandType(typeof(HelpTestCommands)); + + var ctx = new AssertReplyContext(); + Format.Mode = Format.FormatMode.None; + Assert.That(CommandRegistry.Handle(ctx, ".help test-help"), Is.EqualTo(CommandResult.Success)); + ctx.AssertReplyContains("SomeEnum Values: A, B, C"); + } } \ No newline at end of file