Before You Start
This guide builds on the basics covered in Creating a Game Handler. Make sure you're comfortable extending GameHandler and sending text messages before diving into embeds.
Your game handler builds embeds for custom command responses and ad-hoc messages. This guide covers everything embeds can do, from colored team indicators to country flags next to player names.
Build a Basic Embed
Embeds give your messages a colored sidebar and structured fields, so important game events stand out from plain chat in a busy channel.
Before we look at code, let's understand the anatomy of an embed. Every embed has three core parts:
- Title: the bold heading at the top. This is what people see first, so make it descriptive.
- Description: the main body text below the title. You can use Discord markdown here for bold, italic, and more.
- Color: the colored bar on the left side of the embed. You set it with
R,G, andBvalues (0–255). This is a subtle but powerful way to convey meaning: red for danger, green for success, blue for info.
Here's how you build one. You create a main JsonObject for the embed itself, a second one for the color, attach the color to the embed, and publish it to the v1/tunnelcast/relay/embed topic:
local JsonObject Json, Color;
Json = new class'JsonObject';
Json.AddString("Title", "Server Event");
Json.AddString("Description", "Something cool just happened!");
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(); Notice the two Clear() calls at the end. You created two JsonObject instances (the main embed and the color), so you need to clean up both. If you skip the Color property entirely, Tunnelcast uses its default purple (RGB 73, 35, 255).
Add Fields
Fields are the most powerful part of embeds. They let you show structured data like a dashboard. Think of them as labeled key-value pairs that appear inside the embed body. Each field has three properties:
- Name: the label, shown in bold above the value.
- Value: the content displayed beneath the label.
- Inline: controls the layout. When set to
true, fields sit side by side (up to 3 per row), which is great for compact stats like map name, player count, and game type. When set tofalse, the field takes the full width, which works better for longer text like player lists.
The pattern for adding fields is a bit different from simple properties. You build a dynamic array of JsonObject instances, one per field, and then attach the whole array to the embed with AddArrayJson():
local JsonObject Json;
local array<JsonObject> Fields;
local int i;
Json = new class'JsonObject';
Json.AddString("Title", "Match Info");
Fields.Length = 3;
Fields[0] = new class'JsonObject';
Fields[0].AddString("Name", "Map");
Fields[0].AddString("Value", "DM-Deck17");
Fields[0].AddBool("Inline", true);
Fields[1] = new class'JsonObject';
Fields[1].AddString("Name", "Game Type");
Fields[1].AddString("Value", "Deathmatch");
Fields[1].AddBool("Inline", true);
Fields[2] = new class'JsonObject';
Fields[2].AddString("Name", "Players");
Fields[2].AddString("Value", "12 / 16");
Fields[2].AddBool("Inline", true);
Json.AddArrayJson("Fields", Fields);
EventGrid.SendEvent("v1/tunnelcast/relay/embed", Json);
// Clean up
for(i = 0; i < Fields.Length; i++)
Fields[i].Clear();
Json.Clear(); Notice what happened here: you now have multiple JsonObject instances in play: the main embed and each field in the array. All of them need Clear() called. The easiest pattern is a cleanup loop at the end, which is exactly what the code above does.
Clear() every JsonObject, including each field, to avoid leaks. (See the Game Handler guide for why this is manual.) Country Flags
Tunnelcast can turn a player's IP address into their country's flag emoji automatically. You need no GeoIP database in your mod, and you don't need to know which country the player is from. The flow works like this:
- Your handler extracts the player's IP from their network connection.
- You wrap it in
{IP:country-flag}syntax in your embed text. - You send the embed to Tunnelcast.
- The server does a GeoIP lookup and replaces the placeholder with the right flag emoji before forwarding it to Discord.
Let's build a helper function called FormatPlayerName(). There are a few things to handle:
GetPlayerNetworkAddress()returns something like192.168.1.1:7777, so you need to strip the port by finding the colon and taking only the left part.StripIllegalCharacters()handles player names with special characters (quotes, backslashes) that could break JSON serialization.- The
{IP:country-flag}placeholder is resolved server-side, so your handler doesn't need to know which country the player is from. That's the server's job.
function string FormatPlayerName(PlayerController PC)
{
local string IP, Name;
local int ColonIndex;
// Get player IP (strip port number)
IP = PC.GetPlayerNetworkAddress();
ColonIndex = InStr(IP, ":");
if(ColonIndex != -1)
IP = Left(IP, ColonIndex);
// Sanitize player name for JSON
Name = class'JsonLib.JsonUtils'.static.StripIllegalCharacters(
PC.PlayerReplicationInfo.PlayerName);
// {IP:country-flag} is resolved server-side to a flag emoji
return "{" $ IP $ ":country-flag} " $ Name;
}Custom & Standard Emojis
Discord has two types of emojis, and Tunnelcast handles both, but they work differently.
Standard emojis are the ones built into Discord. They're available everywhere and you don't need any special setup. Just use their shortcode directly in your embed text: :fire:, :skull:, :trophy:. Discord renders them on its end, so Tunnelcast just passes them through without any processing.
Custom emojis are specific to a Discord server, namely the one linked to your game server in Tunnelcast. Maybe your community has a custom pepe emoji or a mod-specific icon. To use these, wrap the emoji name in {name:emoji} syntax. The server looks up the emoji by name in the linked Discord server and replaces it with the full Discord emoji reference.
// Custom server emojis: resolved from the linked Discord server
Json.AddString("Description", "{pepe:emoji} Player scored a goal!");
// Standard Discord emojis: work directly, no special syntax needed
Json.AddString("Description", ":fire: Player is on a streak!");
Json.AddString("Description", ":red_square: 5 - 3 :blue_square:");Here are some standard emoji shortcodes that work well for game servers. These are just suggestions. You can use any standard Discord emoji shortcode in your embeds.
| Shortcode | Description |
|---|---|
:red_square: | Red team indicator |
:blue_square: | Blue team indicator |
:fire: | Streak / hot |
:skull: | Death |
:trophy: | Winner |
:crossed_swords: | Combat / PvP |
Want to see Tunnelcast's own custom icons? The Rich Icons Explorer lists every application emoji we use, grouped by category, with live previews and copy-ready :name: shortcodes.
Line Breaks & Server IP
Two utility features that come up in almost every handler: line breaks and server IP substitution.
Line breaks are how you build multi-line content inside an embed field. In UnrealScript, \\n in a string literal produces a literal backslash-n. Tunnelcast's server sees this and converts it to an actual line break before sending to Discord. This is the most common pattern modders use: looping through a player array and concatenating names with \\n between them:
PlayersText = "";
for(i = 0; i < Players.Length; i++)
{
PlayersText $= FormatPlayerName(Players[i]) $ "\n";
}Server IP substitution solves a practical problem. Your game server knows its local address, but players need the public IP to connect. The {0.0.0.0:ip} syntax tells Tunnelcast to substitute the real public IP from the authenticated session. This means you don't hardcode IPs. If the server moves to a different host, everything still works.
Fields[0] = new class'JsonObject';
Fields[0].AddString("Name", "Join");
Fields[0].AddString("Value", "{" $ Level.GetAddressURL() $ ":ip}");
Fields[0].AddBool("Inline", false);{0.0.0.0:ip}, your handler never needs to know its own public IP. Tunnelcast fills it in automatically. Images, Links & Timestamps
A few extra properties let you polish your embeds even further. Each one is optional, so use them when they add value to your message:
- ImageUrl: displays an image at the bottom of the embed. Great for showing a map preview or your mod's logo.
- Url: makes the embed title a clickable link. Useful for linking to your server's page or a leaderboard.
- ShowTimestamp: adds a timestamp to the embed footer so players know exactly when the event happened. Particularly useful for score updates where timing matters.
Json.AddString("ImageUrl", "https://example.com/banner.png");
Json.AddString("Url", "https://example.com/server");
Json.AddBool("ShowTimestamp", true);Here's a complete reference of every property you can set on an embed:
| Property | Type | Description |
|---|---|---|
Title | String | Bold heading at the top of the embed |
Description | String | Body text below the title (supports Discord markdown) |
Color | JsonObject | RGB color for the left sidebar (R, G, B: 0–255) |
Fields | Array | Structured data fields with Name, Value, and Inline |
ImageUrl | String | URL of an image to display at the bottom |
Url | String | Makes the title a clickable hyperlink |
ShowTimestamp | Bool | Adds a timestamp to the embed footer |
Special Variables Reference
Here's a quick-reference table of all the special variables you can use in embed text. Keep this handy while building your handlers.
| Syntax | Example | Result |
|---|---|---|
{IP:country-flag} | {8.8.8.8:country-flag} | Flag emoji (e.g., |
{0.0.0.0:ip} | {0.0.0.0:7777:ip} | Real server IP with port |
{name:emoji} | {pepe:emoji} | Custom Discord emoji |
:shortcode: | :fire: | Standard Discord emoji |
\\n | Line1\\nLine2 | Line break |
**text** | **bold** | Discord markdown (bold) |
Full Example: Team Score Tracker
Let's put everything together with a real handler that you'd actually use. Imagine you're running a team game. Every time a team scores, you want Discord to show a colored embed with the score, player lists with country flags, and a timestamp.
Here's the complete function. Let's walk through the design decisions:
function SendTeamScoresEmbed(int ScoringTeamIndex)
{
local JsonObject Json, Color;
local array<JsonObject> Fields;
local int i, RedScore, BlueScore;
local string Title, Description;
RedScore = TeamGame.Teams[0].Score;
BlueScore = TeamGame.Teams[1].Score;
// Score line with emoji squares
Description = ":red_square: " $ RedScore $ " - " $ BlueScore $ " :blue_square:";
// Dynamic title based on game state
if(ScoringTeamIndex == 0)
{
Title = "Red Team scores!";
Color = new class'JsonObject';
Color.AddInt("R", 221);
Color.AddInt("G", 46);
Color.AddInt("B", 68);
}
else
{
Title = "Blue Team scores!";
Color = new class'JsonObject';
Color.AddInt("R", 85);
Color.AddInt("G", 172);
Color.AddInt("B", 238);
}
Json = new class'JsonObject';
Json.AddString("Title", Title);
Json.AddString("Description", Description);
Json.AddJson("Color", Color);
Json.AddBool("ShowTimestamp", true);
// Built-in helper adds player lists with country flags
EnrichEmbedWithPlayers(Fields);
Json.AddArrayJson("Fields", Fields);
EventGrid.SendEvent("v1/tunnelcast/relay/embed", Json);
// Always clean up JSON objects
for(i = 0; i < Fields.Length; i++)
Fields[i].Clear();
Json.Clear();
Color.Clear();
}Let's break down why this works:
- Different colors for different teams: red and blue embeds give instant visual feedback about who scored, even before reading the text.
EnrichEmbedWithPlayers()is a built-in helper that automatically adds formatted player lists as embed fields, complete with country flags. You don't have to build those fields yourself.ShowTimestampadds a footer timestamp so your community can see exactly when each goal happened.- The cleanup loop at the end is essential. Every
JsonObjectyou created (the embed, the color, and every field added byEnrichEmbedWithPlayers()) needs to be cleared to prevent memory leaks.
MonitorGame(), then call a send function. You'd compare TeamGame.Teams[0].Score against a saved LastRedScore variable each tick, and fire this function only when the score changes. That covers the full embed feature set. Mix and match these features as your handlers need them.
