//
// 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;
}
}
}