Skip to main content

Actions

Actions are code snippets executed when a log event is imported from the game server. Use them to process data and automate responses.

Actions now support both a server-local workflow and a marketplace workflow. For the shared model behind marketplace, installed, and custom actions, see Action marketplace concept.

Data source note: actions depend on imported server logs. If logs arrive later, action execution is also delayed. For full import/freshness behavior, see Logs import concept.

  • Server Admin -> Actions

Tabs

The actions screen is split into three tabs:

  • Marketplace: browse public creator actions and install them on the current server
  • Installed: manage marketplace actions already installed on this server
  • Custom Actions: create and maintain server-owned actions with local Lua code

Installed marketplace actions inherit their code, event trigger, and config schema from the creator definition. Server staff only manage the active state and configuration values for that server.

Access requirements

Actions now have three permission levels:

  • actions.edit: Allows editing server-side configuration values and enabling or disabling installed or custom actions
  • actions.manage_marketplace: Allows installing marketplace actions and removing installed marketplace actions
  • actions.edit_code: Allows creating and maintaining custom actions, including the Lua code and configuration schema

This separation allows server administrators to distinguish between operational action settings, marketplace management, and local code authoring.

Action language: Lua

The actions are written in Lua, a lightweight, high-level, multi-paradigm programming language designed primarily for embedded use in applications.

Action scripts run directly. Two globals are available:

  • data: Event payload of the current log event
  • config: Resolved action configuration values

To preview values while building scripts, use the print function.

Here is an example of a simple action that prints the log event data to the console:

print(data);

Configuration fields

Actions support user-defined configuration fields that allow you to parameterize your Lua scripts without editing the code. This is useful for:

  • Allowing moderators to adjust action behavior without code access
  • Reusing the same action template with different settings
  • Keeping sensitive values (like channel IDs) separate from the code

For marketplace actions, these fields are defined by the creator. Server admins only set the values in the installed server copy.

Defining configuration fields

For custom actions, configuration fields are defined in the Code tab using the configuration schema builder.

For marketplace actions, configuration fields are defined in Creator Space by the action author and then appear on each server installation.

Each field has:

  • Name: Key used in Lua (config.<name>)
  • Type: see Field types below
  • Default: Fallback value used when no value is saved in Settings
  • Description: Help text shown in the Settings tab

Recommended naming for field names: camelCase or snake_case without spaces.

Setting configuration values

After a field is defined in the Code tab, users with actions.edit can set its value in the Settings tab.

At runtime, config values are resolved as:

  1. Saved value from Settings tab (if present)
  2. Otherwise the field default from schema
  3. Otherwise nil

Only fields defined in the schema are exposed in config.

Field types

TypeLua valueNotes
stringstring|nilSingle-line text; empty → nil
textareastring|nilMulti-line text; identical to string in Lua
emailstring|nilEmail address input; identical to string in Lua
numbernumber|nilParsed as int or float
booleanbool|nilCheckbox; empty → nil
coordinatetable|nilMap picker — access .x, .z, optionally .y, .radius
generic_selectstring|nil or string[]Dropdown from a fixed list; string[] when multiple: true
faction_selectstring|nil or string[]Server's faction IDs
player_selectstring|nil or string[]Server's player usernames
relation_selectstring[]Always an array; never nil
discord_channel_selectstring|nil or string[]Discord text channel IDs
discord_category_selectstring|nil or string[]Discord category IDs
discord_role_selectstring|nil or string[]Discord role IDs
discord_guild_selectstring|nil or string[]Discord guild IDs
object_listtable[]Repeatable list of objects; never nil — empty returns {}

Empty values are treated as nil unless noted otherwise.

Using configuration in Lua

Configuration values are available via the config table:

-- Access configuration values
discord.send_to_channel(config.channelId, "Player killed: " .. data.victim.username);

if config.notifyOnHeadshot and data.weapon == "Mosin" then
discord.send_to_channel(config.channelId, "Mosin kill detected");
end

if data.distance > config.minDistanceForBonus then
player.change_balance(data.killer.username, config.bonusAmount, "Long range kill bonus");
end

Example configuration

For a kill notification action, you might define:

NameTypeDefaultDescription
channelIdstringDiscord channel ID for notifications
notifyOnHeadshotbooleantrueSend extra message for headshots
minDistanceForBonusnumber200Minimum distance for bonus reward
bonusAmountnumber50Currency bonus for long-range kills

Moderators can then adjust these values in the Settings tab without needing access to the code.

Functions

To customize the action taken on a new log entry, the code editor provides searchable autocomplete using [CTRL] + [SPACE].

The following functions are available to use in the actions:

Prints the data to the console.

print({name = "John", age = 30}); -- this will be printed as json
print("Hello, World!");

Server namespace

The server namespace contains all server-related functions.

server.name()

Returns the name of the server.

print(server.name);

server.current_time()

Returns the current time on the server.

print(server.current_time);

server.ban_player(username: string, reason: string = "", length: string = "99 years")

Creates a ban restriction for the player with the given username. The ban is visible and manageable in Admin → Restrictions.

server.ban_player("John");
server.ban_player("John", "Cheating");
server.ban_player("John", "Cheating", "1 day");

Discord namespace

The discord namespace contains all Discord-related functions.

Attention: All Discord ID must be represented as strings (encapsulated in quotes), otherwise the programming language will interpret it as a Number and represents ints mathematically instead of the raw number and your action will fail.

discord.send_to_player(username: string, content: string)

Sends a message to the player with the given username.

discord.send_to_player("John", "Hello, John!");
discord.send_to_player("h3eh23ieuh3ei2he3", "Hello, John!"); -- throws an "player not found" exception that stops the action

discord.send_to_channel(channel: string, content: string)

Sends a message to the channel with the given ID.

discord.send_to_channel("1234567890", "Hello, World!");

discord.mention(username: string)

Returns the mention string for the player with the given username. This can be used within discord messages to reference the discord account. If no player is found, it will return the username with no formatting.

print(discord.mention("John")); -- returns "<@1234567890>"
print(discord.mention("h3eh23ieuh3ei2he3")); -- returns "h3eh23ieuh3ei"

Player namespace

The player namespace contains all player-related functions.

player.change_balance(username: string, amount: number, description: string = "")

Changes the balance of the player with the given username.

player.change_balance("John", 100);
player.change_balance("John", 100, "Nice shot!");
player.change_balance("John", -100, "Terrible shot!"); -- negative amount to subtract

player.statistics(username: string)

Returns a statistics object for the player with the given username. Returns nil if no matching player exists.

local stats = player.statistics("John");
print(stats.online_streak); -- prints the current online day streak

Return fields:

FieldTypeDescription
pvp_killsintTotal PvP kills
last_pvp_killstring|nilTimestamp of last kill
pvp_deathsintTotal PvP deaths
last_pvp_deathstring|nilTimestamp of last PvP death
pvp_kdfloatKill/death ratio
deathsintTotal deaths (all causes)
kill_streakintKills since last death
death_streakintDeaths since last kill
longest_killfloatLongest kill distance in metres
online_streakintConsecutive calendar days with at least one session (including today)

Example output:

{
"pvp_kills": 8,
"last_pvp_kill": "2024-06-19 22:32:23",
"pvp_deaths": 23,
"last_pvp_death": "2024-06-19 22:49:19",
"pvp_kd": 0.35,
"deaths": 58,
"kill_streak": 0,
"death_streak": 2,
"longest_kill": 44.78,
"online_streak": 3
}
Example: Online streak reward

Give a bonus that scales with how many consecutive days the player has been online:

local stats = player.statistics(data.player.username);

if stats.online_streak >= 3 then
local bonus = config.base_reward + (stats.online_streak * config.streak_bonus);
player.change_balance(data.player.username, bonus, "Online-Streak: " .. stats.online_streak .. " Tage");
else
player.change_balance(data.player.username, config.base_reward, "Tägliche Belohnung");
end

player.get_faction(username: string)

Returns a faction data object for the player's current faction, or nil if the player was not found or is not in a faction.

local faction = player.get_faction("John");

if faction then
print(faction.id); -- e.g. 42
print(faction.name); -- e.g. "Wolves"
end

Return fields:

FieldTypeDescription
idintFaction ID
namestringFaction name
colorstring|nilFaction colour hex code
imagestring|nilFaction image path
balanceint|nilCurrent faction treasury balance
discord_role_idstring|nilLinked Discord role ID

Example output:

{
"id": 42,
"name": "Wolves",
"color": "#ff0000",
"image": null,
"balance": 1500,
"discord_role_id": "1234567890"
}
Example: Reward an entire faction on a kill
local faction = player.get_faction(data.killer.username);

if faction then
faction.change_balance(faction.id, config.faction_reward, "Kill reward");
discord.send_to_channel(config.channel, faction.name .. " earned " .. config.faction_reward .. " for a kill!");
end
note

player.get_faction previously returned the faction ID as a plain string. It now returns an object — use .id to access the ID.

player.get_name(username: string)

Resolves the player's display name. If no name is set for the user or no matching user is found, it returns the input username.

player.get_name("johnnyboy420"); -- outputs the display name of the player: "John Doe"
player.get_faction("nouserwiththatusername"); -- user is not found, so it returns the input: "nouserwiththatusername"

player.get_main_account(username: string)

Resolves the main account username for a player. Walks through all players that share at least one device with the given player and returns the username of the first one that has a user account linked. If the player itself has a linked user account, its own username is returned. If no linked account can be found or the player is unknown, the input username is returned unchanged.

player.get_main_account("alt_account");   -- returns "main_account" if the alt shares a device with the main
player.get_main_account("solo_player"); -- returns "solo_player" if no linked main account exists
Example: Credit rewards to the main account
local main = player.get_main_account(data.killer.username);
player.change_balance(main, config.reward or 50, "Kill reward (via " .. data.killer.username .. ")");

player.has_discord_role(username: string, role: string)

Checks if the player with the given username has the specified Discord role. Accepts both the role name and the role ID.

player.has_discord_role("John", "6553535632356535564"); -- works with role IDs
player.has_discord_role("John", "Admin"); -- works with role names as well

player.ensure_discord_role(username: string, roleId: string)

Grants the specified Discord role to the player. If the player already has the role, nothing happens. The role must be identified by its Discord role ID.

player.ensure_discord_role("John", "1234567890123456789");

player.remove_discord_role(username: string, roleId: string)

Removes the specified Discord role from the player. If the player does not have the role, nothing happens. The role must be identified by its Discord role ID.

player.remove_discord_role("John", "1234567890123456789");

player.list(filters?: table)

Returns a list of all players on the server, optionally filtered. Calling it without arguments returns every player.

local players = player.list();

local vip_players = player.list({ group = "vip" });

local active_with_discord = player.list({
has_discord = true,
last_online_after = "-7d",
});

Available filters (all optional):

FilterTypeDescription
groupstringOnly players who are a member of this group (by name)
whitelistedboolOnly whitelisted / non-whitelisted players
bannedboolOnly players who have an active ban restriction / players without one
has_discordboolOnly players with a linked Discord account
has_balance_abovenumberOnly players whose balance is above this value
has_balance_belownumberOnly players whose balance is below this value
last_online_afterstringOnly players last seen after this time. Supports relative formats like "-7d", "-2h", "-1m", "-1y" or an absolute datetime string
last_online_beforestringOnly players last seen before this time. Same format as above
in_factionbool or stringtrue = in any faction, false = in no faction, or a faction name/id to match a specific faction
variablestring or tableOnly players who have a variable with this name set. Pass a string to match by name only, or a table { name = "...", value = "..." } to also match a specific value

Return fields (one entry per matching player):

FieldTypeDescription
idintInternal player ID
usernamestringPlayer username
balanceint|nilCurrent balance
last_onlinestring|nilLast seen timestamp (Y-m-d H:i:s)
whitelistedboolWhether the player is whitelisted
bannedboolWhether the player has an active ban restriction
Example: Daily balance reward for all VIP members
local players = player.list({
group = "vip",
last_online_after = "-30d",
});

for _, p in ipairs(players) do
player.change_balance(p.username, 100, "Monthly VIP bonus");
end
Example: Notify inactive players via Discord
local inactive = player.list({
has_discord = true,
last_online_before = "-14d",
banned = false,
});

for _, p in ipairs(inactive) do
discord.send_to_player(p.username, "We miss you on the server! Come back soon.");
end
Example: Filter by variable value
-- All players whose "vip_tier" variable is set to "gold"
local gold_vips = player.list({ variable = { name = "vip_tier", value = "gold" } });

for _, p in ipairs(gold_vips) do
player.change_balance(p.username, 500, "Gold VIP daily bonus");
end

player.get_variable(username: string, name: string)

Returns the value of a player variable, or nil if the variable is not set.

local result = player.get_variable("John", "kill_count");
print(result.value); -- prints the stored value, or nil

Return fields:

FieldTypeDescription
namestringVariable name
valuestring|nilStored value, or nil if not set

player.set_variable(username: string, name: string, value: string|nil)

Creates or updates a player variable. Pass nil as value to clear it.

player.set_variable("John", "kill_count", "42");
player.set_variable("John", "vip_tier", "gold");
player.set_variable("John", "temp_flag", nil); -- clears the variable
Example: Track kill streaks with variables
local streak_result = player.get_variable(data.killer.username, "kill_streak");
local streak = tonumber(streak_result.value) or 0;
streak = streak + 1;

player.set_variable(data.killer.username, "kill_streak", tostring(streak));

if streak >= 5 then
player.change_balance(data.killer.username, 250, "Kill streak x" .. streak);
discord.send_to_channel(config.feedChannel, data.killer.username .. " is on a " .. streak .. "-kill streak!");
end

-- Reset the victim's streak
player.set_variable(data.victim.username, "kill_streak", "0");

Faction namespace

The faction namespace contains all faction related action functions.

faction.change_balance(id: string, amount: number, description: string = "")

Changes the balance of the faction with the given id.

faction.change_balance("123", 100);
faction.change_balance("123", 100, "Good guys!");
faction.change_balance("123", -100, "Rule penalty!"); -- negative amount to subtract

Math namespace

The math namespace contains utility functions for calculations.

math.distance(x1: number, z1: number, x2: number, z2: number)

Calculates the 2D distance between two points. Useful for checking if a position is within a certain radius of another position.

-- Calculate distance between two coordinates
local dist = math.distance(1000, 2000, 1500, 2500);
print("Distance: " .. dist); -- outputs approximately 707.1
Example: Radius check

Check if a kill happened within a certain radius of a location (e.g., a safezone or base):

local safezone_x = 5000;
local safezone_z = 5000;
local safezone_radius = 100;

local kill_distance = math.distance(data.x, data.z, safezone_x, safezone_z);

if kill_distance < safezone_radius then
discord.send_to_channel(config.alertChannel, "Kill detected inside safezone!");
server.ban_player(data.killer, "Killing in safezone", "1 day");
end
Example: Reward long-distance kills
-- Calculate distance between killer and victim
local kill_range = math.distance(data.killerX, data.killerZ, data.victimX, data.victimZ);

if kill_range > 500 then
player.change_balance(data.killer, 100, "Long range kill bonus (" .. math.floor(kill_range) .. "m)");
discord.send_to_channel(config.feedChannel, data.killer .. " scored a " .. math.floor(kill_range) .. "m kill!");
end

HTTP namespace

The http namespace allows actions to make outbound HTTP requests to external services. Requests have a 5-second timeout.

http.get(url: string, headers?: table)

Performs an HTTP GET request. Returns two values: the HTTP status code and the response body as a string.

local status, body = http.get("https://api.example.com/data");
print(status); -- e.g. 200
print(body); -- raw response body string

With custom headers:

local status, body = http.get("https://api.example.com/data", {
["Authorization"] = "Bearer mytoken",
["Accept"] = "application/json",
});

http.post(url: string, body?: string, headers?: table)

Performs an HTTP POST request. Returns two values: the HTTP status code and the response body as a string.

local status, body = http.post("https://api.example.com/webhook", "payload=hello");

Sending a JSON body:

local payload = json.encode({ event = "kill", player = data.killer.username });
local status, body = http.post("https://api.example.com/events", payload, {
["Content-Type"] = "application/json",
["Authorization"] = "Bearer mytoken",
});
Example: Notify an external webhook on a kill event
local payload = json.encode({
killer = data.killer.username,
victim = data.victim.username,
weapon = data.weapon,
});

local status, body = http.post(config.webhookUrl, payload, {
["Content-Type"] = "application/json",
});

if status ~= 200 then
print("Webhook failed with status: " .. status);
end

JSON namespace

The json namespace provides JSON encoding and decoding to work with API responses and payloads.

json.encode(value)

Encodes a Lua value (table, string, number, boolean) as a JSON string.

local encoded = json.encode({ name = "John", score = 42 });
print(encoded); -- {"name":"John","score":42}

json.decode(json: string)

Parses a JSON string and returns a Lua table.

local decoded = json.decode('{"name":"John","score":42}');
print(decoded.name); -- John
print(decoded.score); -- 42
Example: Call a JSON API and use the response
local status, body = http.get("https://api.example.com/players/" .. data.player.username);

if status == 200 then
local result = json.decode(body);
if result.vip == true then
player.change_balance(data.player.username, config.vipBonus, "VIP bonus");
end
end