Your first commands

Turn a static method into a command with one attribute.

Anatomy of a command

A command is a static method with the [Command] attribute. Its parameters are the parameters of the command. That's all it takes:

using MI.Console;
using UnityEngine;

// Wallet stands for your game's own code
public static class CheatCommands
{
    [Command("give-gold")]
    [CommandSummary("Add gold to the player's purse")]
    static void GiveGold([ParamInfo("How much gold")] int amount)
    {
        Wallet.Gold += amount;
        Debug.Log($"Gold: {Wallet.Gold}");
    }
}
> give-gold 500
Gold: 1500
  • The method can be public, internal or private.
  • Its class can be any class, static or not.
  • What the method returns, if anything, is logged: static int Gold() => Wallet.Gold; logs gold: 1500.

Where commands can live

Declared onRunsPage
A static method or propertyOnce, as typedThis page, and Parameters
A method or property of a componentOn the objects of the scene you name: @Enemy* heal 20Commands on objects
An extension method of GameObject or of a componentOn the objects of the scene you nameCommands on objects

Use [SecuredCommand] instead of [Command] for a command that must ask for a password: see Protected commands.

Names

Without a name, a command is named after its method in kebab-case: SetLanguage is typed set-language, HTTPGet is http-get. Names are case-insensitive.

Give the name yourself: [Command("set-lang")]. Renaming the method then doesn't rename the command, which your aliases, scripts and MirageConsole.Execute calls may rely on.

  • A name can have letters, digits, underscores and hyphens (not first).
  • Two commands with the same name are reported as a conflict, and only the first is registered.

How commands are found

The console finds the commands by reflection when the game starts. It only looks at the assemblies that reference it (yours, the samples), not Unity's or other packages', and logs what it found:

[Console] 18 commands registered: 4 of 234 assemblies scanned in 2 ms
  • Your commands must be in an assembly loaded when the game starts. A command in an assembly you load later isn't found.
  • If your code uses assembly definitions (.asmdef), the assembly with your commands must reference MirageInteractive.Console.
  • A command that can't be registered (a parameter of a type the console can't read, a name already taken...) is reported in Unity's Console, and the others still work.
  • [DoNotLookForCommand] on a class makes the console skip it.

To see them without starting the game, open Tools > Mirage Console > Check Commands. The window finds the commands the same way the console does when the game starts, and lists them group by group, with their syntax, summary, aliases and where they are declared. Above them it lists the problems: what the console would log (a name already taken, a parameter of a type it can't read, an alias in conflict, a method that returns an IEnumerator), an alias that can't run, the commands without a [CommandSummary], and a group default that holds 20 commands or more. It checks again after each compilation, and on Check. In the Editor it also sees the commands of your Editor assemblies, which a build doesn't have.

Writing output

Write to the console with Unity's usual methods: Debug.Log, Debug.LogWarning, Debug.LogError. What a command logs while it runs goes to the CLI (and to All Messages, like every message of the game). Start your messages with a tag to sort them: Debug.Log("[Economy] Gold: 1500").

What the method returns is logged too, after the name of the command: static int Gold() logs gold: 1500. Don't also log the value you return, or it shows twice.

Coroutines and async methods

A command runs at once, on the main thread, and the console doesn't wait for it:

  • A method that returns an IEnumerator isn't run as a coroutine: none of its body runs. The console warns about it when it registers the command, and so does Check Commands. Make the command return void and start the coroutine itself, with StartCoroutine on a MonoBehaviour.
  • An async method runs until its first await, then goes on by itself. What it logs after that still shows, but what it returns isn't logged, and an exception it throws after the await isn't reported as an error of the command.

Refusing a value

The console checks that each value converts to the type of its parameter. When your command needs more (a range, a value that isn't empty...), throw an InvalidParameterValueException. The console reports it with the syntax of the command, like a value it couldn't convert:

using System.Globalization;   // CultureInfo

[Command("volume")]
static void SetVolume(float percent)
{
    if (percent < 0f || percent > 100f)
        throw new InvalidParameterValueException("volume", "percent",
            percent.ToString(CultureInfo.InvariantCulture), "a percentage, from 0 to 100");

    AudioListener.volume = percent / 100f;
}
> volume 150
[Command Error] '150' isn't a valid value for 'percent' (a percentage, from 0 to 100). Type 'help volume' for more information
Usage: volume <percent>

Any other exception your command throws is logged with its stack trace, and the console keeps working.

Going further