Describe chat commands in a machine readable way
Some checks failed
.NET Core / build (push) Has been cancelled
Some checks failed
.NET Core / build (push) Has been cancelled
The only description of a chat command was its usage string, which is meant to be read by a human. A user interface which wants to offer the commands to a player needs the parts separately: the command, what it does, and one entry per parameter. Add ChatCommandInfo and ChatCommandParameterInfo, built from the metadata which is already there - ChatCommandHelpAttribute for the command and its required character status, ArgumentAttribute for the short names and whether a parameter is required, ValidValuesAttribute for the accepted values. Name and description come from the display attribute, so they are returned in the language of the player. The description which is passed to ChatCommandHelpAttribute was never stored anywhere. Keep it as a fallback for commands which have no display attribute, instead of discarding it. The reset info command had its texts hard coded in English. It now refers to resources like every other chat command does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MSpK6jkyF8ZS5nYXyGwYxA
This commit is contained in:
@@ -39,6 +39,7 @@ public class ChatCommandHelpAttribute : Attribute
|
||||
public ChatCommandHelpAttribute(string command, string description, Type? argumentsType)
|
||||
: this(command, argumentsType, CharacterStatus.Normal)
|
||||
{
|
||||
this.Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -64,6 +65,15 @@ public class ChatCommandHelpAttribute : Attribute
|
||||
/// </summary>
|
||||
public CharacterStatus MinimumCharacterStatus { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the command, if one was specified.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only a fallback for commands without a <see cref="DisplayAttribute"/>.
|
||||
/// A description defined there can be translated, so it's preferred.
|
||||
/// </remarks>
|
||||
public string? Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the arguments of the chat command.
|
||||
/// </summary>
|
||||
|
||||
28
src/GameLogic/PlugIns/ChatCommands/ChatCommandInfo.cs
Normal file
28
src/GameLogic/PlugIns/ChatCommands/ChatCommandInfo.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
// <copyright file="ChatCommandInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Describes a chat command in a machine-readable way.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In contrast to <see cref="ChatCommandHelpAttribute.Usage"/>, 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.
|
||||
/// </remarks>
|
||||
/// <param name="Command">The command, including the slash, e.g. <c>/item</c>.</param>
|
||||
/// <param name="Name">The name of the command, in the requested language.</param>
|
||||
/// <param name="Description">The description of the command, in the requested language.</param>
|
||||
/// <param name="MinimumCharacterStatus">The character status which is required to execute the command.</param>
|
||||
/// <param name="Usage">The usage text, as it's shown by the help command.</param>
|
||||
/// <param name="Parameters">The parameters of the command, in the order in which they are expected when they are passed without their short names.</param>
|
||||
public record ChatCommandInfo(
|
||||
string Command,
|
||||
string Name,
|
||||
string Description,
|
||||
CharacterStatus MinimumCharacterStatus,
|
||||
string Usage,
|
||||
IReadOnlyList<ChatCommandParameterInfo> Parameters);
|
||||
@@ -0,0 +1,21 @@
|
||||
// <copyright file="ChatCommandParameterInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
|
||||
|
||||
/// <summary>
|
||||
/// Describes one parameter of a chat command in a machine-readable way, so that
|
||||
/// a user interface can generate an input field for it.
|
||||
/// </summary>
|
||||
/// <param name="Name">The name of the parameter, as defined by the property of the arguments class.</param>
|
||||
/// <param name="ShortName">The short name which is used in the <c>shortName=value</c> notation, if the parameter is decorated with an <see cref="ArgumentAttribute"/>; Otherwise, <see langword="null"/>.</param>
|
||||
/// <param name="TypeName">The name of the value type, e.g. <c>Byte</c> or <c>String</c>.</param>
|
||||
/// <param name="IsRequired">A value indicating whether the parameter has to be specified to execute the command.</param>
|
||||
/// <param name="ValidValues">The accepted values, if the parameter only accepts a limited set of them; Otherwise, empty.</param>
|
||||
public record ChatCommandParameterInfo(
|
||||
string Name,
|
||||
string? ShortName,
|
||||
string TypeName,
|
||||
bool IsRequired,
|
||||
IReadOnlyList<string> ValidValues);
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;
|
||||
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods regarding chat command types.
|
||||
@@ -18,11 +21,104 @@ public static class ChatCommandTypeExtensions
|
||||
/// <returns>The available chat commands of the player.</returns>
|
||||
public static IEnumerable<ChatCommandHelpAttribute> GetAvailableChatCommands(this Player player)
|
||||
{
|
||||
return player.GameContext?.PlugInManager
|
||||
.GetKnownPlugInsOf<IChatCommandPlugIn>()
|
||||
.Select(CustomAttributeExtensions.GetCustomAttribute<ChatCommandHelpAttribute>)
|
||||
.Where(attribute => attribute is { })
|
||||
.Where(attribute => player.SelectedCharacter?.CharacterStatus >= attribute!.MinimumCharacterStatus)
|
||||
.Select(attribute => attribute!) ?? Enumerable.Empty<ChatCommandHelpAttribute>();
|
||||
return GetAvailableCommands(player).Select(command => command.Help);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the available chat commands of the player,
|
||||
/// in the language of the player.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <returns>The described chat commands which are available to the player.</returns>
|
||||
public static IEnumerable<ChatCommandInfo> GetAvailableChatCommandInfos(this Player player)
|
||||
{
|
||||
return GetAvailableCommands(player)
|
||||
.Select(command => CreateChatCommandInfo(command.PlugInType, command.Help, player.Culture));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the description of the chat command which is implemented by the specified plugin type.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">The type of the chat command plugin.</param>
|
||||
/// <param name="culture">The language in which name and description should be returned. If it's <see langword="null"/>, the language of the current thread is used.</param>
|
||||
/// <returns>The described chat command, if the type is a chat command plugin; Otherwise, <see langword="null"/>.</returns>
|
||||
public static ChatCommandInfo? TryCreateChatCommandInfo(Type plugInType, CultureInfo? culture = null)
|
||||
{
|
||||
if (plugInType.GetCustomAttribute<ChatCommandHelpAttribute>() 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<IChatCommandPlugIn>()
|
||||
.Where(plugInManager.IsPlugInActive)
|
||||
.Select(plugInType => (PlugInType: plugInType, Help: plugInType.GetCustomAttribute<ChatCommandHelpAttribute>()))
|
||||
.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<DisplayAttribute>();
|
||||
IReadOnlyList<ChatCommandParameterInfo> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of a <see cref="DisplayAttribute"/> in the requested language.
|
||||
/// </summary>
|
||||
/// <param name="display">The attribute.</param>
|
||||
/// <param name="rawValue">The raw value of the requested property, which is a resource key when a <see cref="DisplayAttribute.ResourceType"/> is set.</param>
|
||||
/// <param name="defaultResolver">The resolver of the attribute itself, which uses the language of the current thread.</param>
|
||||
/// <param name="culture">The requested language.</param>
|
||||
/// <returns>The value in the requested language, if available.</returns>
|
||||
private static string? GetLocalizedValue(DisplayAttribute? display, string? rawValue, Func<string?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,27 +113,46 @@ public static class CommandExtensions
|
||||
/// <param name="argumentsType">Type of the arguments.</param>
|
||||
/// <returns>A list of parameters with name, type, and valid values.</returns>
|
||||
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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the parameters of an argument class.
|
||||
/// </summary>
|
||||
/// <param name="argumentsType">Type of the arguments.</param>
|
||||
/// <returns>The described parameters, in the order in which they are expected when they are passed without their short names.</returns>
|
||||
public static IEnumerable<ChatCommandParameterInfo> GetParameterInfos(Type argumentsType)
|
||||
{
|
||||
var properties = argumentsType.GetProperties().Where(p => p.CanWrite);
|
||||
foreach (var property in properties)
|
||||
{
|
||||
string validValues = string.Empty;
|
||||
IReadOnlyList<string> validValues = [];
|
||||
|
||||
if (property.GetCustomAttribute<ValidValuesAttribute>() 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<ArgumentAttribute>(inherit: true);
|
||||
|
||||
yield return new ChatCommandParameterInfo(
|
||||
property.Name,
|
||||
argumentAttribute?.ShortName,
|
||||
property.PropertyType.Name,
|
||||
argumentAttribute?.IsRequired ?? false,
|
||||
validValues);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
20
src/GameLogic/Properties/PlugInResources.Designer.cs
generated
20
src/GameLogic/Properties/PlugInResources.Designer.cs
generated
@@ -1,4 +1,4 @@
|
||||
//------------------------------------------------------------------------------
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
// Runtime Version:4.0.30319.42000
|
||||
@@ -2267,6 +2267,24 @@ namespace MUnique.OpenMU.GameLogic.Properties {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Handles the chat command '/resetinfo'. Shows the required costs and the granted points for the next reset..
|
||||
/// </summary>
|
||||
public static string ResetInfoChatCommandPlugIn_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("ResetInfoChatCommandPlugIn_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Reset info chat command.
|
||||
/// </summary>
|
||||
public static string ResetInfoChatCommandPlugIn_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("ResetInfoChatCommandPlugIn_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Reset Feature.
|
||||
/// </summary>
|
||||
|
||||
@@ -195,6 +195,12 @@
|
||||
<data name="ResetChatCommandPlugIn_Description" xml:space="preserve">
|
||||
<value>Handles the chat command '/reset'.</value>
|
||||
</data>
|
||||
<data name="ResetInfoChatCommandPlugIn_Name" xml:space="preserve">
|
||||
<value>Reset info chat command</value>
|
||||
</data>
|
||||
<data name="ResetInfoChatCommandPlugIn_Description" xml:space="preserve">
|
||||
<value>Handles the chat command '/resetinfo'. Shows the required costs and the granted points for the next reset.</value>
|
||||
</data>
|
||||
<data name="ResetFeaturePlugIn_Name" xml:space="preserve">
|
||||
<value>Reset Feature</value>
|
||||
</data>
|
||||
|
||||
@@ -14,7 +14,7 @@ using MUnique.OpenMU.PlugIns;
|
||||
/// </summary>
|
||||
[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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user