// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; using System.Globalization; using System.Reflection; /// /// Extensions to make the process of creating more commands easier. /// public static class CommandExtensions { /// /// Parse the arguments of a command string. /// /// The command. /// The player which issued the command. /// The type. /// Returns the parsed object, if successful; Otherwise . public static async ValueTask TryParseArgumentsAsync(this string command, Player? player) where T : class, new() { var instance = new T(); var properties = typeof(T).GetProperties() .Where(property => property.SetMethod is { }) .ToList(); var arguments = command.Split(' ').Where(x => !x.Contains("/")).ToList(); if (command.Contains('=')) { // [Short argument parsing] // If the command string contains = it means it is using the short version if (await ReadNamedArgumentsAsync(instance, properties, arguments, player).ConfigureAwait(false)) { return instance; } return null; } var attributedArguments = properties .Select(p => p.GetCustomAttribute(inherit: true)) .Where(a => a is { }) .Select(a => a!) .ToList(); var requiredArgumentCount = attributedArguments.Any() ? attributedArguments.Count(a => a.IsRequired) : arguments.Count; if (arguments.Count < requiredArgumentCount) { if (player is not null) { await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CommandExtensionsInvalidArgumentCount), requiredArgumentCount, arguments.Count).ConfigureAwait(false); } return null; } var success = true; for (var i = 0; i < Math.Min(arguments.Count, properties.Count); i++) { var property = properties[i]; var argument = arguments[i]; success = await TrySetPropertyValueAsync(instance, property, argument, player).ConfigureAwait(false) && success; } return instance; } /// /// Create the usage string for the command using the argument class. /// /// Type of the arguments. /// The command name. /// /// The usage string. /// public static string CreateUsage(Type argumentsType, string commandName) { var stringBuilder = new StringBuilder(); stringBuilder.Append($"{commandName} "); foreach (var parameter in GetParameters(argumentsType).ToList()) { if (!string.IsNullOrWhiteSpace(parameter.ValidValues)) { stringBuilder.Append($"{{{parameter.Name}:{parameter.ValidValues}}}"); } else if (parameter.Type == nameof(String)) { stringBuilder.Append($"{{{parameter.Name}}}"); } else { stringBuilder.Append($"{{{parameter.Name}:{parameter.Type}}}"); } stringBuilder.Append(" "); } stringBuilder.ToString().TrimEnd(' '); return stringBuilder.ToString(); } /// /// Gets the parameters for an argument class. /// /// Type of the arguments. /// A list of parameters with name, type, and valid values. public static IEnumerable<(string Name, string Type, string ValidValues)> GetParameters(Type argumentsType) { return GetParameterInfos(argumentsType) .Select(parameter => (Name: parameter.Name, Type: parameter.TypeName, ValidValues: string.Join('|', parameter.ValidValues))); } /// /// Gets the description of the parameters of an argument class. /// /// Type of the arguments. /// The described parameters, in the order in which they are expected when they are passed without their short names. public static IEnumerable GetParameterInfos(Type argumentsType) { var properties = argumentsType.GetProperties().Where(p => p.CanWrite); foreach (var property in properties) { IReadOnlyList validValues = []; if (property.GetCustomAttribute() is { } validValuesAttribute) { validValues = validValuesAttribute.ValidValues.ToList(); } else if (property.PropertyType == typeof(bool)) { validValues = ["0", "1"]; } else if (property.PropertyType == typeof(byte) || property.PropertyType == typeof(ushort) || property.PropertyType == typeof(uint)) { // todo: ranges in ParameterAttribute } // A parameter without an ArgumentAttribute can't be required - the parser // only counts the required arguments of the attributed properties. var argumentAttribute = property.GetCustomAttribute(inherit: true); yield return new ChatCommandParameterInfo( property.Name, argumentAttribute?.ShortName, property.PropertyType.Name, argumentAttribute?.IsRequired ?? false, validValues); } } private static async ValueTask ReadNamedArgumentsAsync(object instance, IList properties, IList arguments, Player? player) { var argumentProperties = properties.Where(property => property.GetCustomAttribute() is { }).ToList(); var requiredProperties = argumentProperties.Where(prop => prop.GetCustomAttribute() is { IsRequired: true }).ToList(); foreach (var property in argumentProperties) { var attribute = property.GetCustomAttributes().First(); var argument = arguments.FirstOrDefault(x => x.Split('=').First().Trim() == attribute.ShortName); if (argument is null) { continue; } // Cleans the argument from the short name var argumentValue = argument.Replace($"{attribute.ShortName}=", string.Empty); if (!await TrySetPropertyValueAsync(instance, property, argumentValue, player).ConfigureAwait(false)) { return false; } requiredProperties.Remove(property); } if (!requiredProperties.Any()) { return true; } if (player is null) { return false; } // One or many required properties were not used foreach (var requiredProperty in requiredProperties) { await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CommandExtensions_RequiredArgumentMissing), requiredProperty.Name).ConfigureAwait(false); } return false; } private static async ValueTask TrySetPropertyValueAsync(object instance, PropertyInfo propertyInfo, string stringValue, Player? player) { try { // Special handling of booleans; we want to allow 0 and 1 as valid values. if (propertyInfo.PropertyType == typeof(bool) && int.TryParse(stringValue, out var intBool)) { stringValue = intBool == 1 ? bool.TrueString : bool.FalseString; } propertyInfo.SetValue(instance, Convert.ChangeType(stringValue, propertyInfo.PropertyType, CultureInfo.InvariantCulture)); return true; } catch { if (player is not null) { await player.ShowLocalizedBlueMessageAsync(nameof(PlayerMessage.CommandExtensionsArgumentInvalidType), propertyInfo.Name, propertyInfo.PropertyType.Name).ConfigureAwait(false); } return false; } } }