Guide

Live Status & the /status Command

How Tunnelcast builds /status, the portal live view, and the live-status embed from your handler's match events, explained by dissecting the real CTF handler.

HomeGuidesLive Status & /status

Before You Start

This guide assumes you already have a working GameHandler that compiles and loads. If you don't, start there. This guide picks up where it leaves off.

Creating a Game HandlerPackage setup, skeleton, sending messages
1

The Match Snapshot

The key idea: Tunnelcast keeps a single match snapshot per server: the current status, scores, objectives, and players. You never build /status yourself. Instead, Tunnelcast renders all three of these from that one snapshot:

  • the /status Discord command
  • the live view on the portal
  • the auto-updating live-status embed in your channel

Your handler's only job is to keep that snapshot current by emitting small match events as things change. Send a flag's new state, a team's new score, a phase change, and the backend merges each one into the snapshot and re-renders everything above. Here's what the rendered /status looks like:

Tunnelcast
TunnelcastAPP2:15 PM
My Awesome Server
Map
CTF-Face
Players
6
Red Team
US FragMaster
DE RocketKing
Blue Team
FR AimBot99
NL DodgeGod

Backend-rendered /status, built entirely from snapshot events

2

What You Get for Free

Before you write any snapshot code, know what already happens without you. The base GameHandler reports the essentials for every game, and the built-in handlers cover the standard UT2004 modes:

  • Base GameHandler: match info on load, start/end transitions, and team scores (polled automatically for team games).
  • CTF / Assault / Invasion handlers: flags, objectives, and waves for those modes, out of the box.
#server-status · edited in place

A simulated Team Deathmatch using only what the base handler reports: map, match state, elapsed time and who's on which team. The message is edited in place as the match plays out, with no snapshot code of your own.

If your mod is a standard team or deathmatch game, you emit nothing here. You only write snapshot code when your mod tracks state the base class can't see, like a custom objective, a boss, a special phase. The rest of this guide is for that case.
3

The Match Events

These are the events that build the snapshot. Each updates one slice of it, so send only what changed and the backend merges it in. Reach for one only when the "Emit it yourself when…" column matches your mod:

Event topicUpdatesEmit it yourself when…Key fields
v1/match/state/deltaTop-level match status & phaseYou have a custom status or phase (base sends load, start, map switch, end)Status, Phase
v1/match/team/scoreOne team's scoreYou score teams in a non-standard way (base polls scores for team games)Team, Score, Label, ColorKey
v1/match/objective/stateState of one objective (flag, control point)Your mod has custom objectives (CTF & Assault cover flags and standard objectives)Id, Label, State, OwnerKey
v1/ut2004/match/invasion/waveCurrent Invasion wave / countdownYou build a custom wave-based mode (the Invasion handler covers stock Invasion)N, CountdownSeconds, ClearCountdown
Snapshot vs. feed. The events above update the persistent snapshot, which is what /status and the live view show right now. A separate topic, v1/match/flag/event, instead posts a one-shot activity line ("Red flag taken!") to the feed. You'll see both in action below.
4

Worked Example: How the CTF Handler Hooks In

The best way to understand snapshot events is to read a handler that uses them. GameHandler_CTF ships with Tunnelcast and reports both flags' states. Let's walk through it.

It hooks two methods from the base class. SendMatchInfo() runs once at the start and reports the full picture; MonitorGame() runs every tick and reports changes. Both delegate to one helper, ReportFlagStates():

class GameHandler_CTF extends GameHandler;

function PreBeginPlay()
{
    Super.PreBeginPlay();
    PreviousFlags.Length = 2;

    // Wrong game type? Remove ourselves; another handler can take over.
    if (CTFGame(Level.Game) == None)
    {
        Log("Not a CTF game. Destroying self.", 'Tunnelcast');
        Destroy();
    }
}

// Emit the full picture once, when match info is first sent.
public function SendMatchInfo()
{
    ResetTrackedState();
    Super.SendMatchInfo();    // base reports map, gametype, scores
    ReportFlagStates(true);   // force = emit every flag right now
}

// Re-check every tick; emit only what changed.
public function MonitorGame()
{
    Super.MonitorGame();
    ReportFlagStates(false);
}

Notice the bForce argument: true means "emit everything now" (so the snapshot starts complete), false means "emit only what changed" (so each tick is cheap). This is the same save-compare-react pattern from MonitorGame() in the Game Handler guide, just applied to flags.

5

Capture → Compare → Emit

ReportFlagStates() is the heart of the handler. For each flag it captures the current state, compares it to the state from last tick, and emits an event only when something changed:

function ReportFlagStates(bool bForce)
{
    local FlagSnapshot Current;
    local int TeamIndex;

    for (TeamIndex = 0; TeamIndex < PreviousFlags.Length; TeamIndex++)
    {
        // 1. Capture the flag's state right now.
        Current = CaptureFlagState(GetFlagForTeamIndex(TeamIndex), TeamIndex);
        if (Current.ObjectiveId == "")
            continue;

        // 2. If it just transitioned, post a one-shot feed line.
        if (!bForce)
            SendFlagEventForTransition(TeamIndex, PreviousFlags[TeamIndex], Current);

        // 3. If anything changed, update the persistent snapshot.
        if (bForce || !AreSnapshotsEqual(PreviousFlags[TeamIndex], Current))
            SendFlagState(Current);

        // 4. Remember it, so next tick can compare against it.
        PreviousFlags[TeamIndex] = Current;
    }
}

"Capturing state" just means reading the engine and packing it into a small struct. The interesting part is GetFlagState(), which turns UT2004's internal flag states into the stable strings the backend expects (home, taken, dropped):

function FlagSnapshot CaptureFlagState(CTFFlag Flag, int TeamIndex)
{
    local FlagSnapshot Snapshot;

    Snapshot.ObjectiveId = GetFlagObjectiveId(TeamIndex);  // "red-flag"
    Snapshot.Label       = GetFlagLabel(TeamIndex);        // "Red Flag"
    Snapshot.State       = GetFlagState(Flag);             // home/taken/dropped
    Snapshot.OwnerKey    = GetTeamKey(TeamIndex);          // "red" / "blue"
    return Snapshot;
}

// Translate raw engine state into a stable string the backend understands.
function string GetFlagState(CTFFlag Flag)
{
    if (Flag == None)
        return "home";

    if (Flag.IsInState('Held'))
    {
        if (Flag.Holder != None)
            return "taken";
        return "dropped";
    }

    if (Flag.IsInState('Dropped'))
        return "dropped";

    return "home";
}
The pattern is reusable for anything: read your mod's state into a struct, keep the previous one in a class variable, and compare. That's all "tracking state" means.
6

Two Kinds of Output: Snapshot vs. Feed

The loop above calls two different send functions, and the difference is the most important thing in this guide.

Persistent state goes to v1/match/objective/state. This is the flag's current condition, and it stays on /status and the live view until it changes again:

// Persistent state -> drives /status, the live view, and the live embed.
function SendFlagState(FlagSnapshot Snapshot)
{
    local JsonObject Json;

    Json = new class'JsonObject';
    Json.AddString("Id", Snapshot.ObjectiveId);
    Json.AddString("Label", Snapshot.Label);
    Json.AddString("State", Snapshot.State);
    Json.AddString("OwnerKey", Snapshot.OwnerKey);
    EventGrid.SendEvent("v1/match/objective/state", Json);
}

One-shot events go to v1/match/flag/event. This is a single announcement ("the flag was just taken") that posts one line to the activity feed and is done:

// One-shot event -> a single activity-feed line ("Red flag taken!").
function SendFlagEvent(string Action, int TeamIndex, string PlayerName)
{
    local JsonObject Json;

    Json = new class'JsonObject';
    Json.AddString("Action", Action);        // taken / returned / captured
    Json.AddString("FlagTeam", GetTeamKey(TeamIndex));
    Json.AddString("FlagLabel", GetFlagLabel(TeamIndex));

    if (PlayerName != "")
        Json.AddString("PlayerName", PlayerName);

    EventGrid.SendEvent("v1/match/flag/event", Json);
}
Tunnelcast
TunnelcastAPP2:16 PM
🚩 Red Flag taken by FragMaster!

The flag event as a one-shot line in the activity feed

Don't confuse them. If you only send the feed event, the flag's state never updates on /status. If you only send the snapshot, players never get the "flag taken!" announcement. CTF sends both, on purpose.
7

Apply It to Your Own Mod

Now the payoff. Say your mod has control points instead of flags. You report them exactly the way CTF reports a flag: same topic, same shape. A "Point A" that a team can capture is just an objective with an Id, a State, and an OwnerKey:

// Your mod's custom objective, reported exactly like CTF reports a flag.
function ReportControlPoint(string Id, string Label, string State, string OwnerKey)
{
    local JsonObject Json;

    Json = new class'JsonObject';
    Json.AddString("Id", Id);             // stable id, e.g. "point-a"
    Json.AddString("Label", Label);       // shown in /status, e.g. "Point A"
    Json.AddString("State", State);       // "neutral", "captured", ...
    Json.AddString("OwnerKey", OwnerKey); // "red" / "blue" / ""
    EventGrid.SendEvent("v1/match/objective/state", Json);
    Json.Clear();
}

Call it from your MonitorGame() when a point changes hands, using the capture-compare-emit pattern from Step 5. Tunnelcast renders your control points into /status and the live view alongside everything the base class already reports, with no extra wiring.

Want to send a "Point A captured!" announcement too? Emit a one-shot event the same way CTF emits v1/match/flag/event. That's the feed side of the snapshot-vs-feed split from Step 6.

To format custom command responses and ad-hoc messages with colors, fields, and country flags, continue with:

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