Table of Contents

Commands

The examples use the GameChat engine and Player source from The chat engine.

Commands from the builder

CreateCommand starts a command, and Build registers it. Parameters are numbered in the order you add them:

using BSGameLib.Chat;

var chat = new GameChat();

chat.CreateCommand("roll")
    .WithKeywords("roll", "r")
    .WithRequiredParameter<int>("sides", "Number of sides")
    .WithHelpText("Rolls a die.")
    .WithAction(ctx =>
    {
      var sides = ctx.GetParameterByName<int>("sides").Value;
      Console.WriteLine($"{ctx.SourceTo<Player>().Name} rolled {Random.Shared.Next(1, sides + 1)}");
    })
    .Build();
  • WithKeywords sets what is typed. Without it, the command's lowercase name is its only keyword.
  • WithRequiredParameter<T> is a parameter that must be given. WithParameter<T> is one that may be left out.
  • A value is converted to T with the current culture. A value that won't convert, such as abc for an int, is reported as a wrong parameter, and the command doesn't run.
  • A parameter that was left out has no value: IsSet is false, and Value is default, 0 for an int and null for an int?. Check IsSet to tell a left-out value from a typed 0. A parameter can be read as its nullable form, GetParameterByName<int?>, whether it was declared as int or int?:
using BSGameLib.Chat;

var chat = new GameChat();

chat.CreateCommand("pay")
    .WithRequiredParameter<string>("player", "Who to pay")
    .WithParameter<int>("amount", "How much, default 1")
    .WithAction(ctx =>
    {
      var amount = ctx.GetParameterByName<int>("amount");
      var count = amount.IsSet ? amount.Value : 1;
      Console.WriteLine($"Paying {ctx.GetParameterByName<string>("player").Value} {count}");
    })
    .Build();

Commands from attributes

A command class implements ICommand, takes its keywords in [Command], and takes each parameter as a property with [RequiredParameter] or [OptionalParameter]. The first argument of a parameter attribute is its position, and the second is its name, which defaults to the property's name:

using BSGameLib.Chat;
using BSGameLib.Chat.Attributes;

[Command("give", "g", HelpText = "Gives an item.")]
public class GiveCommand : ICommand
{
  [RequiredParameter(1, "item", HelpText = "What to give")]
  public string Item { get; set; } = "";

  [OptionalParameter(2, "amount", HelpText = "How many", Suggestions = "items")]
  public int? Amount { get; set; }

  public void Execute(ICommandContext context) =>
    Console.WriteLine($"{context.SourceTo<Player>().Name} gives {Item} x{Amount ?? 1}");
}
  • An optional parameter of a value type must be nullable, as int? is. Otherwise registration throws an ArgumentException.
  • The properties are filled in before Execute runs. A new command object is made each time the command is typed, so the arguments of one use never reach the next.
  • On the server, the command's constructor parameters come from dependency injection. On its own, the factory provider creates the command.
  • Suggestions names the values the game knows for a parameter, such as players. A client uses it to complete a line; see Describing commands to players.

Taking the rest of the line

A parameter with CapturesRemainder = true takes everything after it, spaces included. Only the last parameter should set it:

using BSGameLib.Chat;
using BSGameLib.Chat.Attributes;

[Command("say", "s", HelpText = "Says something to everyone.")]
public class SayCommand : ICommand
{
  [RequiredParameter(1, "message", CapturesRemainder = true)]
  public string Message { get; set; } = "";

  public void Execute(ICommandContext context) => Console.WriteLine($"Say: {Message}");
}

With that command, /s hello big world gives Message the value hello big world.

Command groups

A group is a keyword followed by one of several sub-commands. Each sub-command has [CommandGroup] with the group's name and keywords, and its own [Command]. The sub-command with IsDefaultCommand = true runs when the group is typed with nothing after it. A default can't take parameters.

using BSGameLib.Chat;
using BSGameLib.Chat.Attributes;

[CommandGroup("party", "p", IsDefaultCommand = true)]
[Command("list", "l")]
public class PartyList : ICommand
{
  public void Execute(ICommandContext context) => Console.WriteLine("Party: Ann, Ben");
}

[CommandGroup("party", "p")]
[Command("say", "s")]
public class PartySay : ICommand
{
  [RequiredParameter(1, "message", CapturesRemainder = true)]
  public string Message { get; set; } = "";

  public void Execute(ICommandContext context) => Console.WriteLine($"To the party: {Message}");
}

With these, /p and /p l list the party, and /p s hello there says something to it. The group's name, party, is not typed unless you list it as a keyword.

The builder makes a group the same way. WithCommand adds a command built with CreateCommand, and WithDefaultAction names the one that runs when the group is typed alone. As with the attribute, that command can't take parameters:

using BSGameLib.Chat;

var chat = new GameChat();

var list = chat.CreateCommand("list")
    .WithKeywords("l")
    .WithAction(ctx => Console.WriteLine("Party: Ann, Ben"))
    .Build();

chat.CreateCommandGroup("party")
    .WithKeywords("p")
    .WithCommand(list)
    .WithDefaultAction("list")
    .Build();

Registering attribute commands

On its own, register each class with RegisterCommand<TCommand>():

using BSGameLib.Chat;

var chat = new GameChat();
chat.RegisterCommand<GiveCommand>();

A set of classes can be given through an ICommandProvider, which returns their types. The engine reads the providers when its Initialize() runs. Calling Initialize() again does nothing. Registering a class both ways also throws. Names and keywords must be unique: a clash throws an ArgumentException when the command is registered.