Guide

Custom Discord Commands

Add custom commands to your game handler that players invoke with /customcommand and make them show up in the /commands list.

HomeGuidesCustom Discord Commands

Before You Start

This guide assumes you already have a working GameHandler that compiles and loads on your server. If you haven't built one yet, start with the game handler guide. It covers creating the package, extending GameHandler, and getting everything compiled.

Creating a Game HandlerStep-by-step guide

By the end of this guide, you'll have custom slash commands that respond with rich embeds and appear in the /commands list.

1

Understand the Command Flow

Commands are just EventGrid topics with the prefix v1/tunnelcast/command/. Tunnelcast has a few built-in Discord slash commands like /status and /commands. For anything custom, players use the /customcommand slash command followed by a name, for example /customcommand top. Here's what happens behind the scenes:

  1. Someone types /customcommand top in Discord.
  2. The Tunnelcast server sends a message to EventGrid with topic v1/tunnelcast/command/top.
  3. MutTunnelcastEventGridSubscriber receives it, since it subscribes to the v1/tunnelcast/command/ prefix.
  4. It calls GameHandler.HandleCommand("v1/tunnelcast/command/top").
  5. Your handler checks the topic string and routes to the right function.
  6. Your function responds by sending a v1/tunnelcast/relay/embed or v1/tunnelcast/relay/text event.

The base GameHandler already handles one command out of the box:

  • v1/tunnelcast/command/commands: calls HandleCommandListCommand()
  • Anything else: calls SendCommandNotAvailable() (red error embed)

Here's what that routing looks like in the base class:

// This is what the base GameHandler does
function HandleCommand(string Topic)
{
    if(Topic == "v1/tunnelcast/command/commands")
    {
        HandleCommandListCommand();
    }
    else
    {
        SendCommandNotAvailable();
    }
}
Your GameHandler doesn't need to know whether a command came from /status (built-in) or /customcommand top (custom). It just sees the EventGrid topic. The /commands list is built by HandleCommandListCommand(), and it's just an embed that lists what's available. You control what shows up there by overriding that method.
2

Add Your First Custom Command

Let's add a top command that shows the top 3 players by score. Players will invoke it with /customcommand top in Discord. To add a command, you override HandleCommand() in your handler. Check for your topic first, then call Super.HandleCommand() for anything you don't recognize. This keeps /status and /commands working.

function HandleCommand(string Topic)
{
    if(Topic == "v1/tunnelcast/command/top")
    {
        HandleTopCommand();
    }
    else
    {
        Super.HandleCommand(Topic);
    }
}
Always call Super.HandleCommand(Topic) in the else branch. That's how /status and /commands keep working. Without it, those built-in commands will silently stop responding.

Now write the actual command handler. This function builds a Discord embed with a gold sidebar, lists the top 3 players sorted by score, and includes a timestamp:

function HandleTopCommand()
{
    local JsonObject Json, Color;
    local array<JsonObject> Fields;
    local int i;
    local string PlayerList;

    // Sort players by score and build list
    PlayerList = "";
    for(i = 0; i < Min(3, GetNumPlayers()); i++)
    {
        PlayerList $= "**" $ (i + 1) $ ".** "
            $ class'JsonLib.JsonUtils'.static.StripIllegalCharacters(
                GetSortedPlayerName(i))
            $ ": " $ GetSortedPlayerScore(i) $ " pts\\n";
    }

    Json = new class'JsonObject';
    Json.AddString("Title", ":trophy: Top Players");
    Json.AddString("Description", PlayerList);

    Color = new class'JsonObject';
    Color.AddInt("R", 255);
    Color.AddInt("G", 215);
    Color.AddInt("B", 0);
    Json.AddJson("Color", Color);

    Json.AddBool("ShowTimestamp", true);
    EventGrid.SendEvent("v1/tunnelcast/relay/embed", Json);

    Json.Clear();
    Color.Clear();
}
3

Register It in the Commands List

Adding the command handler alone isn't enough, because players need to know it exists. When someone types /commands, they should see your new command listed. Override HandleCommandListCommand() to include your new command in the response:

function HandleCommandListCommand()
{
    local JsonObject Json, Color;
    local string Description;

    Json = new class'JsonObject';
    Json.AddString("Title", "Available Commands");

    Color = new class'JsonObject';
    Color.AddInt("R", 73);
    Color.AddInt("G", 35);
    Color.AddInt("B", 255);
    Json.AddJson("Color", Color);

    Description = "`/status` - Server status with player list\\n";
    Description $= "`/customcommand top` - Top 3 players by score\\n";
    Description $= "`/commands` - Show this list\\n";

    Json.AddString("Description", Description);
    EventGrid.SendEvent("v1/tunnelcast/relay/embed", Json);

    Json.Clear();
    Color.Clear();
}
4

Add More Commands

The pattern for adding commands is always the same: add a topic check in HandleCommand(), write the handler function, and add the entry to HandleCommandListCommand(). Let's add a /customcommand mapinfo command that shows current map details:

function HandleCommand(string Topic)
{
    if(Topic == "v1/tunnelcast/command/top")
    {
        HandleTopCommand();
    }
    else if(Topic == "v1/tunnelcast/command/mapinfo")
    {
        HandleMapInfoCommand();
    }
    else
    {
        Super.HandleCommand(Topic);
    }
}

And the handler that builds the map info embed:

function HandleMapInfoCommand()
{
    local JsonObject Json, Color;

    Json = new class'JsonObject';
    Json.AddString("Title", ":map: "
        $ class'JsonLib.JsonUtils'.static.StripIllegalCharacters(
            GetMapTitle()));
    Json.AddString("Description",
        "**Game Type:** " $ Level.Game.GameName $ "\\n"
        $ "**Players:** " $ GetNumPlayers()
            $ " / " $ Level.Game.MaxPlayers $ "\\n"
        $ "**Time:** "
            $ Level.Game.GameReplicationInfo.ElapsedTime $ "s");

    Color = new class'JsonObject';
    Color.AddInt("R", 46);
    Color.AddInt("G", 204);
    Color.AddInt("B", 113);
    Json.AddJson("Color", Color);

    EventGrid.SendEvent("v1/tunnelcast/relay/embed", Json);
    Json.Clear();
    Color.Clear();
}
Remember to add /customcommand mapinfo to your HandleCommandListCommand() as well. Otherwise players won't know it exists even though it works.
5

Handle Unknown Commands Gracefully

The base SendCommandNotAvailable() sends a red embed with "Command not available or not recognized." It's functional, but not very helpful. You can override it for a friendlier message that points users to /commands:

function SendCommandNotAvailable()
{
    local JsonObject Json, Color;

    Json = new class'JsonObject';
    Json.AddString("Title", "Unknown Command");
    Json.AddString("Description",
        "That command doesn't exist. Try `/commands` to see what's available.");

    Color = new class'JsonObject';
    Color.AddInt("R", 237);
    Color.AddInt("G", 66);
    Color.AddInt("B", 69);
    Json.AddJson("Color", Color);

    EventGrid.SendEvent("v1/tunnelcast/relay/embed", Json);
    Json.Clear();
    Color.Clear();
}
Command names are case-insensitive on the Discord side, but the topic comparison in UnrealScript uses == which is case-sensitive. The topics always arrive lowercase from the server, so use lowercase strings in your comparisons.

Here's a summary of the complete command routing flow:

What HappensWhere
User types /customcommand top in DiscordDiscord → Tunnelcast Server
Server publishes to EventGridTopic: v1/tunnelcast/command/top
EventGridSubscriber receives itMutTunnelcastEventGridSubscriber.ProcessEvent()
Routed to your handlerGameHandler.HandleCommand("v1/tunnelcast/command/top")
Your code respondsSend embed via v1/tunnelcast/relay/embed

Every custom command follows the same three-step pattern: route the topic, build the embed, register it in the commands list. For more on building embed responses, see the Discord Embeds guide:

Discord Embeds & Advanced FeaturesColors, fields, flags, and more