diff --git a/src/GameLogic/PlugIns/ChatCommands/ChatCommandHelpAttribute.cs b/src/GameLogic/PlugIns/ChatCommands/ChatCommandHelpAttribute.cs index b66e3ee..b8ad53e 100644 --- a/src/GameLogic/PlugIns/ChatCommands/ChatCommandHelpAttribute.cs +++ b/src/GameLogic/PlugIns/ChatCommands/ChatCommandHelpAttribute.cs @@ -39,6 +39,7 @@ public class ChatCommandHelpAttribute : Attribute public ChatCommandHelpAttribute(string command, string description, Type? argumentsType) : this(command, argumentsType, CharacterStatus.Normal) { + this.Description = description; } /// @@ -64,6 +65,15 @@ public class ChatCommandHelpAttribute : Attribute /// public CharacterStatus MinimumCharacterStatus { get; } + /// + /// Gets the description of the command, if one was specified. + /// + /// + /// This is only a fallback for commands without a . + /// A description defined there can be translated, so it's preferred. + /// + public string? Description { get; } + /// /// Gets the type of the arguments of the chat command. /// diff --git a/src/GameLogic/PlugIns/ChatCommands/ChatCommandInfo.cs b/src/GameLogic/PlugIns/ChatCommands/ChatCommandInfo.cs new file mode 100644 index 0000000..b52edea --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/ChatCommandInfo.cs @@ -0,0 +1,28 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; + +/// +/// Describes a chat command in a machine-readable way. +/// +/// +/// In contrast to , which is a text +/// meant to be read by a human, this description keeps the parts of a command +/// separated. That allows a user interface to offer the commands of a player +/// without requiring the player to know or type any of them. +/// +/// The command, including the slash, e.g. /item. +/// The name of the command, in the requested language. +/// The description of the command, in the requested language. +/// The character status which is required to execute the command. +/// The usage text, as it's shown by the help command. +/// The parameters of the command, in the order in which they are expected when they are passed without their short names. +public record ChatCommandInfo( + string Command, + string Name, + string Description, + CharacterStatus MinimumCharacterStatus, + string Usage, + IReadOnlyList Parameters); diff --git a/src/GameLogic/PlugIns/ChatCommands/ChatCommandParameterInfo.cs b/src/GameLogic/PlugIns/ChatCommands/ChatCommandParameterInfo.cs new file mode 100644 index 0000000..f832c6c --- /dev/null +++ b/src/GameLogic/PlugIns/ChatCommands/ChatCommandParameterInfo.cs @@ -0,0 +1,21 @@ +// +// Licensed under the MIT License. See LICENSE file in the project root for full license information. +// + +namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; + +/// +/// Describes one parameter of a chat command in a machine-readable way, so that +/// a user interface can generate an input field for it. +/// +/// The name of the parameter, as defined by the property of the arguments class. +/// The short name which is used in the shortName=value notation, if the parameter is decorated with an ; Otherwise, . +/// The name of the value type, e.g. Byte or String. +/// A value indicating whether the parameter has to be specified to execute the command. +/// The accepted values, if the parameter only accepts a limited set of them; Otherwise, empty. +public record ChatCommandParameterInfo( + string Name, + string? ShortName, + string TypeName, + bool IsRequired, + IReadOnlyList ValidValues); diff --git a/src/GameLogic/PlugIns/ChatCommands/ChatCommandTypeExtensions.cs b/src/GameLogic/PlugIns/ChatCommands/ChatCommandTypeExtensions.cs index b1b8b99..62bd338 100644 --- a/src/GameLogic/PlugIns/ChatCommands/ChatCommandTypeExtensions.cs +++ b/src/GameLogic/PlugIns/ChatCommands/ChatCommandTypeExtensions.cs @@ -4,7 +4,10 @@ namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands; +using System.Globalization; using System.Reflection; +using System.Resources; +using MUnique.OpenMU.PlugIns; /// /// Extension methods regarding chat command types. @@ -18,11 +21,104 @@ public static class ChatCommandTypeExtensions /// The available chat commands of the player. public static IEnumerable GetAvailableChatCommands(this Player player) { - return player.GameContext?.PlugInManager - .GetKnownPlugInsOf() - .Select(CustomAttributeExtensions.GetCustomAttribute) - .Where(attribute => attribute is { }) - .Where(attribute => player.SelectedCharacter?.CharacterStatus >= attribute!.MinimumCharacterStatus) - .Select(attribute => attribute!) ?? Enumerable.Empty(); + return GetAvailableCommands(player).Select(command => command.Help); } -} \ No newline at end of file + + /// + /// Gets the description of the available chat commands of the player, + /// in the language of the player. + /// + /// The player. + /// The described chat commands which are available to the player. + public static IEnumerable GetAvailableChatCommandInfos(this Player player) + { + return GetAvailableCommands(player) + .Select(command => CreateChatCommandInfo(command.PlugInType, command.Help, player.Culture)); + } + + /// + /// Creates the description of the chat command which is implemented by the specified plugin type. + /// + /// The type of the chat command plugin. + /// The language in which name and description should be returned. If it's , the language of the current thread is used. + /// The described chat command, if the type is a chat command plugin; Otherwise, . + public static ChatCommandInfo? TryCreateChatCommandInfo(Type plugInType, CultureInfo? culture = null) + { + if (plugInType.GetCustomAttribute() is not { } help) + { + return null; + } + + return CreateChatCommandInfo(plugInType, help, culture); + } + + private static IEnumerable<(Type PlugInType, ChatCommandHelpAttribute Help)> GetAvailableCommands(Player player) + { + var plugInManager = player.GameContext?.PlugInManager; + if (plugInManager is null) + { + return []; + } + + return plugInManager + .GetKnownPlugInsOf() + .Where(plugInManager.IsPlugInActive) + .Select(plugInType => (PlugInType: plugInType, Help: plugInType.GetCustomAttribute())) + .Where(command => command.Help is { }) + .Where(command => player.SelectedCharacter?.CharacterStatus >= command.Help!.MinimumCharacterStatus) + .Select(command => (command.PlugInType, Help: command.Help!)); + } + + private static ChatCommandInfo CreateChatCommandInfo(Type plugInType, ChatCommandHelpAttribute help, CultureInfo? culture) + { + var display = plugInType.GetCustomAttribute(); + IReadOnlyList parameters = help.ArgumentsType is { } argumentsType + ? CommandExtensions.GetParameterInfos(argumentsType).ToList() + : []; + + return new ChatCommandInfo( + help.Command, + GetLocalizedValue(display, display?.Name, () => display?.GetName(), culture) ?? plugInType.Name, + GetLocalizedValue(display, display?.Description, () => display?.GetDescription(), culture) ?? help.Description ?? string.Empty, + help.MinimumCharacterStatus, + help.Usage, + parameters); + } + + /// + /// Gets the value of a in the requested language. + /// + /// The attribute. + /// The raw value of the requested property, which is a resource key when a is set. + /// The resolver of the attribute itself, which uses the language of the current thread. + /// The requested language. + /// The value in the requested language, if available. + private static string? GetLocalizedValue(DisplayAttribute? display, string? rawValue, Func defaultResolver, CultureInfo? culture) + { + if (display is null || string.IsNullOrEmpty(rawValue)) + { + return null; + } + + if (culture is { } + && display.ResourceType is { } resourceType + && resourceType.GetProperty(nameof(PlugInResources.ResourceManager), BindingFlags.Public | BindingFlags.Static)?.GetValue(null) is ResourceManager resourceManager + && resourceManager.GetString(rawValue, culture) is { } localizedValue) + { + return localizedValue; + } + + // Either no specific language was requested, or the value is a literal which + // can't be translated anyway - the attribute knows how to resolve both. + try + { + return defaultResolver(); + } + catch (InvalidOperationException) + { + // The attribute refers to a resource which doesn't exist. A single + // misconfigured plugin shouldn't break the whole command list. + return null; + } + } +} diff --git a/src/GameLogic/PlugIns/ChatCommands/CommandExtensions.cs b/src/GameLogic/PlugIns/ChatCommands/CommandExtensions.cs index e081649..fe6c94e 100644 --- a/src/GameLogic/PlugIns/ChatCommands/CommandExtensions.cs +++ b/src/GameLogic/PlugIns/ChatCommands/CommandExtensions.cs @@ -113,27 +113,46 @@ public static class CommandExtensions /// 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) { - string validValues = string.Empty; + IReadOnlyList validValues = []; if (property.GetCustomAttribute() is { } validValuesAttribute) { - validValues = string.Join('|', validValuesAttribute.ValidValues); + validValues = validValuesAttribute.ValidValues.ToList(); } else if (property.PropertyType == typeof(bool)) { - validValues = "0|1"; + validValues = ["0", "1"]; } else if (property.PropertyType == typeof(byte) || property.PropertyType == typeof(ushort) || property.PropertyType == typeof(uint)) { // todo: ranges in ParameterAttribute - // validValues = ""; } - yield return (property.Name, property.PropertyType.Name, validValues); + // 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); } } diff --git a/src/GameLogic/Properties/PlugInResources.Designer.cs b/src/GameLogic/Properties/PlugInResources.Designer.cs index 1ce7bf1..c91308b 100644 --- a/src/GameLogic/Properties/PlugInResources.Designer.cs +++ b/src/GameLogic/Properties/PlugInResources.Designer.cs @@ -1,4 +1,4 @@ -//------------------------------------------------------------------------------ +//------------------------------------------------------------------------------ // // This code was generated by a tool. // Runtime Version:4.0.30319.42000 @@ -2267,6 +2267,24 @@ namespace MUnique.OpenMU.GameLogic.Properties { } } + /// + /// Looks up a localized string similar to Handles the chat command '/resetinfo'. Shows the required costs and the granted points for the next reset.. + /// + public static string ResetInfoChatCommandPlugIn_Description { + get { + return ResourceManager.GetString("ResetInfoChatCommandPlugIn_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reset info chat command. + /// + public static string ResetInfoChatCommandPlugIn_Name { + get { + return ResourceManager.GetString("ResetInfoChatCommandPlugIn_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Reset Feature. /// diff --git a/src/GameLogic/Properties/PlugInResources.resx b/src/GameLogic/Properties/PlugInResources.resx index 541f2a2..429b327 100644 --- a/src/GameLogic/Properties/PlugInResources.resx +++ b/src/GameLogic/Properties/PlugInResources.resx @@ -195,6 +195,12 @@ Handles the chat command '/reset'. + + Reset info chat command + + + Handles the chat command '/resetinfo'. Shows the required costs and the granted points for the next reset. + Reset Feature diff --git a/src/GameLogic/Resets/ResetInfoChatCommandPlugIn.cs b/src/GameLogic/Resets/ResetInfoChatCommandPlugIn.cs index 625a6a6..9470f3f 100644 --- a/src/GameLogic/Resets/ResetInfoChatCommandPlugIn.cs +++ b/src/GameLogic/Resets/ResetInfoChatCommandPlugIn.cs @@ -14,7 +14,7 @@ using MUnique.OpenMU.PlugIns; /// [Guid("79F2C2C2-2E4C-4F4B-8A74-4227D1209D27")] [PlugIn] -[Display(Name = "Reset Info Command", Description = "Shows required costs and granted points for the next reset.")] +[Display(Name = nameof(PlugInResources.ResetInfoChatCommandPlugIn_Name), Description = nameof(PlugInResources.ResetInfoChatCommandPlugIn_Description), ResourceType = typeof(PlugInResources))] [ChatCommandHelp(Command, "Shows required costs and gained points for the next reset.", null)] public class ResetInfoChatCommandPlugIn : IChatCommandPlugIn {