Commands

Write ::commands with ICommandScript - names and aliases, parsing arguments, replying to the player, staff ranks, built-in commands and module bridges.

9 min read

What a command is#

A command is text a player types into the chat box starting with two colons, such as ::heal or ::give 995 100. The client sends it as a chat line; the server sees the :: prefix, strips it, and routes the rest to a command handler instead of broadcasting it as chat. Commands are the quickest way to add debugging tools, admin powers, and player-facing utilities (::home, ::daily, ::stats) without touching any UI.

You add a command by writing one C# class that implements ICommandScript and saving it in server/scripts/commands/. The server compiles the file at boot, registers the command under its Name and Aliases, and logs [CommandRegistry] Registered command: ::name. No other wiring is needed.

Anatomy of a command script#

C#
/// <summary>
/// Heals the player. Usage: ::heal [amount]
/// </summary>
public class HealCommand : ICommandScript
{
    public string Name => "heal";
    public string[] Aliases => new[] { "hp", "health" };
    public string Description => "Heals the player by an amount (default: full heal)";
    public string Usage => "heal [amount]";

    public async Task Execute(ScriptPlayer player, string[] args)
    {
        if (args.Length > 0 && float.TryParse(args[0], out float amount))
            player.Heal(amount);
        else
            player.SetHealth(player.MaxHealth);

        await player.SendGameMessage($"Health: {player.Health}/{player.MaxHealth}");
        _ = player.PlayGraphic(2);
    }
}
MemberRequiredMeaning
string NameYesThe command without ::. Matching is case-insensitive.
string[] AliasesNo (default: none)Extra names that run the same command, for example hp and health for heal.
string DescriptionYesOne line describing the command. Listed by the built-in ::scripts command in the server console.
string UsageNo (default: Name)A usage pattern such as give <itemId> [amount]. Reply with it when the arguments are wrong; there is no built-in player-facing help command.
int RequiredRankNo (default: 0)Minimum staff rank needed to run it: 0 everyone, 1 moderator, 2 admin, 3 owner. Enforced before Execute is called.
Task Execute(ScriptPlayer player, string[] args)YesThe body. player is the caller; args is everything after the command name. Return Task.CompletedTask from a synchronous body, or mark the method async and await inside it.

No using lines are needed: the server prepends its prelude, which already covers ICommandScript, ScriptPlayer, ScriptWorld, GameData, the module system and the usual System namespaces. See the scripting overview for the full list.

A single file may contain several command classes; the starter's SkillCommands.cs declares six. Every class in the file that implements ICommandScript is registered.

Create and test a command#

  1. Open the Script Editor and click New Script (or the + tab).
  2. Select the Command kind, type a name such as Daily and click Create. The file server/scripts/commands/Daily.cs opens with a working stub: Name, Description, Usage and an Execute that sends a message.
  3. Edit the body. Type player. to browse the player API with completion; hover any member for its signature.
  4. Save with Ctrl+S.
  5. Click Dev Test in the top bar. The editor validates every script first and opens a Script Validation dialog if something is malformed; otherwise it starts (or hot-reloads) the local server and launches the client.
  6. Read the local server's console window (Dev Test opens one titled EzRealm Server (Play Local)) and confirm the line [CommandRegistry] Registered command: ::daily. A Compilation errors in Daily.cs: block means the file was skipped - fix the listed lines and run Dev Test again.
  7. In the game, type ::daily in the chat box.

On a hosted test server the same file ships inside the server data zip with every build, so after Push to Test the command is live there too.

Arguments#

The server splits the text after the command name on spaces and drops empty entries, so ::give 995 100 yields args = ["995", "100"]. Nothing is typed or validated for you; treat every argument as a string and parse it yourself.

C#
public Task Execute(ScriptPlayer player, string[] args)
{
    if (args.Length == 0 || !int.TryParse(args[0], out int itemId))
    {
        _ = player.SendGameMessage("Usage: ::" + Usage);
        return Task.CompletedTask;
    }

    int amount = 1;
    if (args.Length > 1 && int.TryParse(args[1], out int parsed))
        amount = parsed;

    if (!ItemExists(itemId))
    {
        _ = player.SendGameMessage($"No item with id {itemId}.");
        return Task.CompletedTask;
    }

    bool ok = player.GiveItem(itemId, amount);
    _ = player.SendGameMessage(ok
        ? $"Added {amount}x {GetItemName(itemId)}."
        : "Your inventory is full.");
    return Task.CompletedTask;
}

Useful patterns:

  • Subcommands. Lower-case args[0] and switch on it (::pc join, ::pc leave). The MCP's command template is built this way.
  • Multi-word values. Join the remaining arguments: string.Join(" ", args[1..]). Display names can contain spaces, so resolve a typed name with player.ResolvePlayerName(text) rather than assuming one token.
  • Numbers. Always use TryParse; a thrown exception inside Execute is caught and written to the server console with its stack trace, and the player gets no reply at all.
  • Random values. The prelude's using static GameData puts a Random(min, max) helper in scope, which shadows the System.Random type. Use Random(1, 7) for a die roll, or System.Random.Shared if you need the .NET class.

Replying to the player#

CallWhere it appears
player.SendGameMessage(text)The caller's chat box, visible only to them. This is the normal reply channel. It returns a Task; await it in an async body or discard it with _ =.
player.Say(text)A normal chat line spoken by the player, with its overhead bubble, as if they had typed it.
player.ForceSay(text)A forced overhead chat line - the same bubble, flagged as forced chat.
player.Narrate(text, onContinue)A narration line in the chatbox dialogue panel.
ScriptWorld.SendMessage(text)A game message to every player online - announcements.
player.Log(text)The server console only, prefixed with the player's username. Players never see it.

The starter's sample commands reply with player.Log, so their output shows up in the server console, not in the game. Switch to SendGameMessage when a command is meant for players.

You can also play feedback: player.PlayAnimation(id, delay), player.PlayGraphic(id) and player.PlayGraphicAt(id, x, y, z) use the ids from the Animations and VFX editors.

Permissions and staff ranks#

Every player carries a rank that is saved with their character:

RankNameTypical use
0PlayerEveryone.
1ModeratorChat moderation.
2AdminAll built-in developer commands: spawn items, teleport, reload scripts.
3OwnerEverything an admin can do, plus granting ranks in game.

Gate a command by overriding RequiredRank:

C#
public class AnnounceCommand : ICommandScript
{
    public string Name => "announce";
    public string Description => "Broadcast a message to every player";
    public string Usage => "announce <message>";
    public int RequiredRank => 1;   // moderators and up

    public async Task Execute(ScriptPlayer player, string[] args)
    {
        if (args.Length == 0)
        {
            await player.SendGameMessage("Usage: ::" + Usage);
            return;
        }
        ScriptWorld.SendMessage($"[{player.DisplayName}] {string.Join(" ", args)}");
    }
}

A player below the required rank receives You don't have permission to use that command. and the attempt is logged on the server. For finer control inside the body, use player.Rank, player.HasRank(2) (true when the rank is at least 2) and player.SetRank(n), which saves immediately.

Ranks are granted from the Creator Dashboard's members page (see Members, roles and staff) or in game by an owner with the built-in ::setrank <username> <0-3>. Any player can check their own rank with ::rank.

How a command is resolved#

When a :: line arrives, the server tries handlers in this order and stops at the first match:

  1. Your command scripts, by Name or alias. The rank check happens here.
  2. Task triggers of type command. A task defined in the Tasks tool (or with define_task) can list command names as its trigger, which starts the task script for the player. See Tasks and quests.
  3. Built-in commands, gated by rank. A built-in the player is not allowed to use answers Unknown command., so the built-in surface is not enumerable by regular players.

Because your scripts are checked first, defining a command with the same name as a built-in overrides it. If two of your scripts register the same name or alias, the later one wins and the console prints a Warning: Overwriting existing command line.

Built-in commands#

The server ships a set of built-ins. Unless listed as player-safe, they require Admin (rank 2).

CommandRankWhat it does
::rank0Shows your rank.
::setname <name>0Claims a display name; ::whois <name> maps a display name back to its account.
::home0Teleports to the home map.
::run, ::togglerun, ::walk0Toggle or force walking/running.
::clearmove, ::moveinfo0Clear the movement queue; print movement debug info to the console.
::gender0Toggles the character's gender.
::setrank <username> <0-3>3Sets another online player's rank.
::item <id> [amount]2Adds an item to your inventory.
::npc <id>, ::obj <id> [size]2Spawns an NPC or object at your position.
::tele <x> <z> [layer] or ::tele <region> [x z layer]2Teleports within the current map or to another map.
::anim <id> [delay], ::gfx <id>, ::proj <tiles>2Play an animation, a graphic, or a test projectile on yourself.
::int <id or name>, ::walkint <id or name>, ::closeint <id>2Open (normal or walkable) or close an interface.
::settext, ::setvisible, ::settransform, ::setitem, ::spawnnode2Drive an open interface's nodes, for testing interface scripts.
::scripts2Lists every registered command script with its description in the server console.
::reloadscripts2Recompiles the scripts/ folder without restarting. Modules are not reloaded; use Dev Test from the editor for a full hot reload.

Built-in output mostly goes to the server console rather than the chat box.

Reaching game systems from a command#

Command scripts compile one file at a time, so a helper in another script is not visible. The supported way to share state is a module: the module owns the logic, the command just calls it.

C#
public class PcCommand : ICommandScript
{
    public string Name => "pc";
    public string Description => "Pest Control lobby";
    public string Usage => "pc join|leave|status";

    public async Task Execute(ScriptPlayer player, string[] args)
    {
        var game = ModuleLoader.Module<PestControlModule>();
        if (game == null)
        {
            await player.SendGameMessage("Pest Control is not available right now.");
            return;
        }

        string sub = args.Length > 0 ? args[0].ToLowerInvariant() : "";
        switch (sub)
        {
            case "join":   game.Join(player.Username); break;
            case "leave":  game.Leave(player.Username); break;
            case "status": await player.SendGameMessage(game.Status()); break;
            default:       await player.SendGameMessage("Usage: ::" + Usage); break;
        }
    }
}

ModuleLoader.Module<T>() returns null when that module failed to compile or was removed, so null-check and degrade instead of throwing. A module declared as ModuleBase<PestControlModule> can also be reached as PestControlModule.Instance. Any public type the module declares (DTOs, enums) is usable from the script, so return real objects rather than delimited strings. Details are in the scripting overview.

Persisting state between sessions#

Static fields in a command script are wiped on every restart and reload. For per-player values use player.SetVar("daily.claimed", tick) with GetVar, GetVarInt, GetVarLong, GetVarBool, HasVar, RemoveVar and IncrementVar; they save with the character. For values shared by everyone, ScriptWorld.GetStore("events") returns a persisted key/value store with the same style of accessors. A ::daily command, for example, compares player.CurrentTick against a stored timestamp, gives the reward with player.GiveItem, and writes the new timestamp back.

Tips and limits#

  • Keep Name lowercase and short; aliases cover the variants.
  • scripts/commands/ is scanned one level deep. A command file inside a subfolder is ignored.
  • The server replies to nothing automatically. If Execute returns without sending a message, the player sees no response - always answer, even on bad input.
  • The built-in ::scripts is the only listing of your commands. If you want an in-game help command, write one: CommandRegistry.Instance.GetAllCommands() returns every registered script with its Name, Description and Usage.
  • The server waits for Execute to finish before it moves on; it is not fire-and-forget. Do not sleep or loop for long inside it. For anything timed, schedule with player.After(ticks, action) or player.Every(ticks, action) (4 ticks is one second) and return.

With AI (MCP)#

write_server_script with kind: "command" writes a ready-to-run ICommandScript into server/scripts/commands/, with a subcommand switch and the ModuleLoader.Module<T>() bridge already sketched in comments. list_server_scripts shows the commands that already exist so new ones follow the same patterns. See the MCP tools reference.

Spotted a mistake or something missing?Tell us on Discord