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
{