Guide

Creating a Plugin

Build server-side plugins for welcome messages, chat filtering, and custom integrations.

HomeGuidesCreating a Plugin

Before You Start

This guide covers TunnelcastPlugin: standalone server-side components that hook into player connections, chat, and EventGrid. Plugins are independent from GameHandlers and don't require a game mod.

If you're new to UnrealScript and haven't set up a package before, start with the Game Handler guide. It covers the fundamentals of package setup and compilation. You don't need a GameHandler to build a plugin, but the workflow is the same.

Creating a Game HandlerUnrealScript basics & package setup
1

Understand Plugins vs GameHandlers

Think of GameHandlers as specialists: each one knows about a specific game type. A handler for Onslaught only runs when Onslaught is active. Plugins are generalists: they run on every server regardless of what game type is active. Use a plugin when your feature isn't tied to a specific game mode.

GameHandlerTunnelcastPlugin
ScopeTied to one game typeRuns on every game type
Loaded viaGameHandlers arrayPlugins array
Best forGame events, scores, match flowChat, player connections, cross-mod features
Needs game mod?YesNo

Plugins are great for things like:

  • Welcome messages for new players
  • Chat filtering: preventing certain messages from reaching Discord
  • Chat formatting: modifying messages before they go to Discord
  • Periodic announcements
  • Custom EventGrid subscribers for cross-mod integration
You can run as many plugins as you want alongside any number of GameHandlers. They don't interfere with each other.

The TunnelcastPlugin Base Class

Every plugin extends TunnelcastPlugin, which itself extends Actor. The base class gives you access to EventGrid and the Tunnelcast mutator, plus a set of lifecycle and chat hooks you can override:

class TunnelcastPlugin extends Actor
    abstract;

var EventGrid EventGrid;
var MutTunnelcast TunnelcastMutator;

public function OnInitialize()
{ }

public function OnPlayerConnected(string Ip, int PlayerIndex)
{ }

public function OnPlayerDisconnected(int PlayerIndex)
{ }

public function bool PreventReportChat(PlayerReplicationInfo PRI, out string Message, name Type)
{
    return false;
}

public function string FormatChatMessage(PlayerReplicationInfo PRI, coerce string Message, name Type)
{
    return Message;
}

You don't need to override every method. Just pick the ones you need. The defaults are safe no-ops: lifecycle hooks do nothing, chat hooks pass everything through unchanged.

2

Create Your Plugin Package

The package structure is the same as a GameHandler: create a folder with a classes subfolder inside your UT2004 directory:

Tunnelcast_MyPlugin/classes/

The key difference from a GameHandler package: you don't need your game mod in Make.ini. Plugins are game-type agnostic, so the only dependencies are JsonLib, EventGrid, and Tunnelcast:

[Editor.EditorEngine]
EditPackages=JsonLib
EditPackages=EventGrid
EditPackages=Tunnelcast
EditPackages=Tunnelcast_MyPlugin
This is simpler than a GameHandler setup because there's no game mod dependency. Your plugin package only depends on Tunnelcast itself.
3

Write a Welcome Message Plugin

Let's build a practical plugin that sends a welcome message to players when they join. Tunnelcast doesn't ship one, which makes it a good first plugin to build and adapt for your own server. (Players are always told their chat is relayed; Tunnelcast does that for you, so your welcome can say whatever you like.)

Create a file called Plugin_Welcome.uc in your classes folder:

class Plugin_Welcome extends TunnelcastPlugin
    config(Tunnelcast);

var config string WelcomeMessage;
var config int WelcomeMessageDelay;

struct PendingWelcome
{
    var PlayerController PC;
    var float SendTime;
};

var array<PendingWelcome> PendingMessages;

function OnInitialize()
{
    if(WelcomeMessage == "")
    {
        log("Plugin_Welcome: No welcome message configured, disabling.", 'Tunnelcast');
        Destroy();
        return;
    }
    SetTimer(1, true);
}

function OnPlayerConnected(string Ip, int PlayerIndex)
{
    local PendingWelcome Welcome;

    Welcome.PC = TunnelcastMutator.Players[PlayerIndex].PC;
    Welcome.SendTime = Level.TimeSeconds + WelcomeMessageDelay;
    PendingMessages[PendingMessages.Length] = Welcome;
}

function Timer()
{
    local int i;

    for(i = PendingMessages.Length - 1; i >= 0; i--)
    {
        if(PendingMessages[i].SendTime <= Level.TimeSeconds)
        {
            if(PendingMessages[i].PC != None)
                SendWelcome(PendingMessages[i].PC);
            PendingMessages.Remove(i, 1);
        }
    }
}

function SendWelcome(PlayerController PC)
{
    local PlayerReplicationInfo SenderPRI;

    if(TunnelcastMutator.ChatSpectator == None)
        return;

    SenderPRI = TunnelcastMutator.ChatSpectator.PlayerReplicationInfo;
    if(Level.Game.BroadcastHandler != None && SenderPRI != None)
        Level.Game.BroadcastHandler.BroadcastText(SenderPRI, PC, WelcomeMessage, 'Say');
}

defaultproperties
{
    WelcomeMessage="Welcome to the server! Type !help for info."
    WelcomeMessageDelay=15
}

That's a lot of code, so let's break down the key concepts:

  • config(Tunnelcast) makes your var config variables editable in Tunnelcast.ini. Server admins can customize the welcome message and delay without touching your code.
  • defaultproperties sets sensible defaults that admins can override in the config file.
  • SetTimer(1, true) creates a repeating 1-second timer that calls the Timer() function.
  • Why the delay? Players need time to fully load in. A message sent immediately can be missed because the player's HUD hasn't finished initializing yet.
  • Iterating backwards: the Timer() loop runs from Length - 1 down to 0 when removing items. This avoids skipping elements, which happens when you remove from a forward loop.
  • TunnelcastMutator.ChatSpectator is a special spectator actor used to send in-game messages. The BroadcastHandler delivers the message to a specific player.
Always check for None before using ChatSpectator and BroadcastHandler. They may not be available during server startup or shutdown.
4

Filter Chat Messages

PreventReportChat() is called for every chat message before it's sent to Discord. Return true to block the message, false to let it through.

Here's a practical example: a plugin that blocks messages starting with ! (a common command prefix for other mods) from reaching Discord:

class Plugin_CommandFilter extends TunnelcastPlugin;

function bool PreventReportChat(PlayerReplicationInfo PRI, out string Message, name Type)
{
    // Block bot commands from reaching Discord
    if(Left(Message, 1) == "!")
        return true;

    return false;
}

You can also modify messages before they reach Discord using FormatChatMessage(). This hook is called after PreventReportChat() passes, so you only format messages that are actually going through:

function string FormatChatMessage(PlayerReplicationInfo PRI, coerce string Message, name Type)
{
    // Could add rank, clan tag, or modify message format
    return "[Player] " $ Message;
}
The call order is: PreventReportChat() is checked first. If it returns false (allow), FormatChatMessage() is then called to optionally modify the message before it goes to Discord.
5

Send Events to Discord

Plugins have full EventGrid access, so they can send text messages and embeds to Discord just like GameHandlers. The same three-step pattern applies: create a JsonObject, fill and send it, then clean up.

Here's a plugin that posts join/leave notifications to Discord:

function SendDiscordAlert(string Message)
{
    local JsonObject Json;
    Json = new class'JsonObject';
    Json.AddString("Text", Message);
    EventGrid.SendEvent("v1/tunnelcast/relay/text", Json);
    Json.Clear();
}

function OnPlayerConnected(string Ip, int PlayerIndex)
{
    local string PlayerName;
    PlayerName = class'JsonLib.JsonUtils'.static.StripIllegalCharacters(
        TunnelcastMutator.Players[PlayerIndex].PC.PlayerReplicationInfo.PlayerName);
    SendDiscordAlert(":wave: **" $ PlayerName $ "** joined the server!");
}

function OnPlayerDisconnected(int PlayerIndex)
{
    SendDiscordAlert(":door: A player left the server.");
}

Note the use of StripIllegalCharacters() on the player name. This sanitizes any characters that could break the JSON payload. Always sanitize player-provided text before sending it through EventGrid.

The v1/tunnelcast/relay/text event also takes an optional Icon property (a rich-icon catalog name; the generic modder icons take a color suffix like flame-amber or trophy-amber) that pins an icon to the line (a Portal-feed @tui.* icon and the matching Discord emoji, or the default relay icon when omitted). The Game Handler guide covers it in full.

Remember to Clear() your JsonObjects after sending to avoid memory leaks. (See the Game Handler guide for why this is manual.)
6

Register Your Plugin

Your plugin is written, but Tunnelcast doesn't know about it yet. Unlike GameHandlers, which are tied to a specific game type, plugins load for every server session. Open your Tunnelcast.ini and add your plugin to the Plugins array:

[Tunnelcast.TunnelcastSettings]
Plugins=Tunnelcast_MyPlugin.Plugin_Welcome

The loading process works like this:

  • Tunnelcast reads the Plugins array from TunnelcastSettings
  • Each plugin class is Spawned as an Actor
  • EventGrid and TunnelcastMutator references are set
  • OnInitialize() is called
  • The plugin runs until the server shuts down (or it Destroy()s itself)
A plugin can remove itself at any time by calling Destroy(). This is useful when your plugin detects an invalid configuration, like the welcome plugin above does when no welcome message is set.
7

Compile & Deploy

Compile with ucc make, copy the .u to your server, and restart. In the Tunnelcast Portal, make sure Text Relay and/or Embed Relay are enabled in your server's Relay Settings.

The portal relay toggles control what gets forwarded to Discord. If your messages aren't showing up, check these first.

After restart, check your server console for 'Tunnelcast' log lines. You should see your plugin loading and initializing.

Plugin API Reference

MethodWhen CalledUse For
OnInitialize()Plugin loadsSetup, validation, timers
OnPlayerConnected(string Ip, int PlayerIndex)Player joinsWelcome messages, logging
OnPlayerDisconnected(int PlayerIndex)Player leavesGoodbye messages, cleanup
PreventReportChat(PRI, Message, Type)Every chat messageFilter/block messages
FormatChatMessage(PRI, Message, Type)Allowed chat messagesModify message content

Available References

PropertyTypeWhat It Gives You
EventGridEventGridPublish events to Discord
TunnelcastMutatorMutTunnelcastServer state, player list
TunnelcastMutator.PlayersarrayConnected player data
TunnelcastMutator.ChatSpectatorMessagingSpectatorSend in-game messages
Level.GameGameInfoCurrent game state

What's Next

Now that you can build plugins, take your Discord messages to the next level with rich embeds: colors, fields, images, and more.

Discord Embeds & Advanced FeaturesRich messages with colors, fields, and more