Skip to content

Provide the available chat commands in a machine-readable form to the client #847

Description

@sven-n

Is your feature request related to a problem? Please describe.

Chat commands are currently only discoverable by typing /list, which prints
one blue system message per command
(ListCommand.cs):

foreach (var commandUsage in commands.Select(x => x.Usage))
{
    await player.ShowBlueMessageAsync(commandUsage).ConfigureAwait(false);
}

That has two problems:

  1. It's not readable. The MuMain client only keeps the last few blue system
    messages on screen and has no scrollback there, so with ~60 registered
    commands the list is gone before the player can read it. This came up in the
    Discord #help channel — a user asked how to find out how to start a Blood
    Castle event on demand, /list was suggested, and the answer was "in the
    case of MuMain /list makes no sense because there is no history scrolling
    in the info window".
  2. It's not discoverable. The player has to know that /list exists in the
    first place, and afterwards has to type every command by hand with the exact
    argument syntax.

The server already knows everything needed to do much better — it just
flattens it into a display string and throws the rest away.

Describe the solution you'd like

Expose the available chat commands to the client in a machine-readable
form, so a client can render a proper UI: browse the commands the player is
actually allowed to use, read a localized description, fill in the parameters
in generated input fields, and execute by clicking. The client-side counterpart
is sven-n/MuMain#539.

What already exists and can be reused

Piece Where
IChatCommandPlugIn (Key, MinCharacterStatusRequirement) src/GameLogic/PlugIns/ChatCommands/IChatCommandPlugIn.cs
ChatCommandHelpAttribute (Command, MinimumCharacterStatus, ArgumentsType, Usage) .../ChatCommandHelpAttribute.cs
ArgumentAttribute (ShortName, IsRequired) .../ArgumentAttribute.cs
ValidValuesAttribute .../ValidValuesAttribute.cs
CommandExtensions.GetParameters(Type)(Name, Type, ValidValues) .../CommandExtensions.cs
GetAvailableChatCommands(this Player) — already filters by CharacterStatus .../ChatCommandTypeExtensions.cs
Localized plugin name/description via [Display(..., ResourceType = typeof(PlugInResources))] on 62 of the 63 command plugins

Also worth noting: executing a command needs no new packet at all.
ChatMessageAction routes every /-prefixed message to
ChatMessageCommandProcessor and never broadcasts it to other players, so the
client can keep sending the composed command line as an ordinary chat message.
Only the listing direction is missing.

Gaps to close first

The good news after an audit of all 63 chat command plugins: the metadata is in
much better shape than the /list output suggests. All 63 have a [Display]
attribute, and 62 of them resolve name and description from
PlugInResources, so descriptions are already there and already localizable.
The remaining work is small and mostly about picking one source of truth:

  1. The description lives in two places, and one of them is thrown away.
    This overload takes a description and never assigns it to a property:

    public ChatCommandHelpAttribute(string command, string description, Type? argumentsType)
        : this(command, argumentsType, CharacterStatus.Normal)

    17 commands pass a description this way that nobody can ever read — and for
    most of them the [Display] description says roughly the same thing in a
    different wording (e.g. ResetInfoChatCommandPlugIn: "…and gained points…"
    vs. "…and granted points…"). Proposal: [Display] is the source of
    truth
    (it's localizable), and the unused ctor parameter gets removed or
    documented as a fallback.

  2. The descriptions mix prose with usage text. 36 of them follow the
    pattern "Handles the chat command '/item <group> <number> …'. Drops a
    specific item next to the character."
    , while others inline a Usage: …
    fragment ("Gets level of a player. Usage: /getlevel (optional:character)").
    Since command and parameters are transmitted structurally, consumers want
    only the second half. Worth normalizing so the UI doesn't have to show the
    syntax twice — see the sequencing note below on when to do that.

  3. Deactivated plugins are still listed. GetAvailableChatCommands uses
    PlugInManager.GetKnownPlugInsOf<IChatCommandPlugIn>(), which returns known
    plugins regardless of their active state, while dispatch in
    ChatMessageCommandProcessor goes through the strategy provider. So a chat
    command deactivated in the admin panel still shows up in /list and /help
    but does nothing when used. This should be filtered with
    PlugInManager.IsPlugInActive. This one is a plain bug and independent of
    everything else here.

  4. One plugin isn't localizable: ResetInfoChatCommandPlugIn uses
    [Display(Name = "Reset Info Command", Description = "…")] with hardcoded
    English instead of ResourceType = typeof(PlugInResources).

  5. Parameter metadata is thin — no per-parameter description and no
    min/max range (there is already a // todo: ranges in ParameterAttribute in
    CommandExtensions.GetParameters). Explicitly not a blocker: name,
    short name, type, required flag and valid values are enough to generate a
    usable input form, and both fields can be added later without breaking the
    packet as long as the strings are length-prefixed and may be empty.

Sequencing

No big metadata project is needed up front. Suggested order:

  1. The IsPlugInActive fix (3 above) — small, independent, a bug today.
  2. The ChatCommandInfo DTO + builder, reading the description from
    [Display], plus fixing the one hardcoded [Display] (4 above).
  3. The admin panel page (Admin panel: dedicated chat commands overview page with usage, parameters and activation toggle #848) as the first consumer — it's pure C#, needs
    no protocol change, no client change and no NuGet release, and it renders
    all 63 descriptions next to their generated usage strings on one screen.
    That is the cheapest place to see which descriptions read badly and to do
    the normalization pass from 2 above with immediate feedback.
  4. Then the packet and the client UI, against a model that has already been
    proven in the browser.

Proposed implementation

a) A DTO built from the existing metadata, e.g. ChatCommandInfo:

  • Command ("/item")
  • DisplayName, Description — localized, resolved for the player's culture
  • MinimumCharacterStatus
  • Parameters: Name, ShortName, Type, IsRequired, ValidValues,
    DefaultValue

b) A view plugin IChatCommandListViewPlugIn in src/GameLogic/Views with
ShowChatCommandListAsync(IReadOnlyCollection<ChatCommandInfo> commands), and
an implementation in src/GameServer/RemoteView annotated with
[MinimumClient(106, 3, ClientLanguage.Invariant)], so only Extended-protocol
clients receive it (MuMain sends version 20404 = "Extended S6E3").

c) Packet definitions in src/Network/Packets:

  • ClientToServer ChatCommandListRequestC1, code 0xF5, sub code 0x00.

  • ServerToClient ChatCommandListC2, code 0xF5, sub code 0x01.

    0xF5 is currently unused in ClientToServerPackets.xml, unused in
    ServerToClientPackets.xml, and unused in MuMain's ProcessPacket switch —
    but the exact code is of course up for discussion.

    Sketch of the payload:

    Field Type Note
    PageIndex / PageCount Byte / Byte so a long list can be split
    CommandCount Byte commands in this packet
    Commands Structure[] variable length, UseCustomIndexer

    and per command entry: EntryLength (ushort), MinimumCharacterStatus
    (byte), ParameterCount (byte), then length-prefixed UTF-8 Command,
    DisplayName, Description, followed by one entry per parameter with
    Flags (required), Type (byte enum), Name, ShortName, ValidValues.

    Variable-length entries in a list are already handled in this codebase —
    AddCharactersToScope does exactly that with UseCustomIndexer plus a
    hand-written partial class.

    Why binary and not JSON: MuMain has no JSON dependency (its CMake only
    pulls in CURL, OpenGL and Python), and the existing XSLT codegen generates
    both the C# writers here and MuMain's C++ bindings straight from these XML
    files. A JSON blob would mean a new third-party dependency and a hand-written
    parser on the client for no real gain.

d) A handler ChatCommandListRequestHandlerPlugIn in
src/GameServer/MessageHandler.

e) Optionally push the list unsolicited right after the character entered
the game world, so the client doesn't even have to ask.

f) A release of MUnique.OpenMU.Network.Packets — MuMain pins version
0.9.9, so the client can only pick this up after a new package is published.

Describe alternatives you've considered

  • Client-side only, parsing the /list output. Fragile (the usage string is
    a display format, not an API), gives no descriptions, no valid values, no
    required/optional distinction, and still depends on the blue-message
    transport that is the actual problem.
  • Just fixing the display of /list (routing it into the scrollable chat
    list box, as suggested in the Discord thread). Worth doing anyway as a small
    independent fix, but it only makes the wall of text scrollable — the player
    still has to know about /list and still has to type every command.
  • JSON over an existing packet. See above — new client dependency, and it
    bypasses the packet definition/codegen infrastructure.

Additional context

Client-side counterpart: sven-n/MuMain#539

Open questions for discussion:

  • Is 0xF5 an acceptable head code, or should this hang off an existing
    extension code with a new sub code?
  • Confirm [Display] as the single source of truth for the description, and
    drop the unused description parameter of ChatCommandHelpAttribute?
  • Do we want per-parameter descriptions and value ranges right away, or in a
    follow-up?
  • Paging vs. a single C2 packet — 63 commands with descriptions should still
    fit into the 65535 byte limit, but paging is cheap insurance against servers
    with many custom command plugins.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions