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.
Navigation path
Server Admin->Actions
Tabs
The actions screen is split into three tabs:
Marketplace: browse public creator actions and install them on the current serverInstalled: manage marketplace actions already installed on this serverCustom 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 eventconfig: 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:
- Saved value from Settings tab (if present)
- Otherwise the field default from schema
- Otherwise
nil
Only fields defined in the schema are exposed in config.
Field types
| Type | Lua value | Notes |
|---|---|---|
string | string|nil | Single-line text; empty → nil |
textarea | string|nil | Multi-line text; identical to string in Lua |
email | string|nil | Email address input; identical to string in Lua |
number | number|nil | Parsed as int or float |
boolean | bool|nil | Checkbox; empty → nil |
coordinate | table|nil | Map picker — access .x, .z, optionally .y, .radius |
generic_select | string|nil or string[] | Dropdown from a fixed list; string[] when multiple: true |
faction_select | string|nil or string[] | Server's faction IDs |
player_select | string|nil or string[] | Server's player usernames |
relation_select | string[] | Always an array; never nil |
discord_channel_select | string|nil or string[] | Discord text channel IDs |
discord_category_select | string|nil or string[] | Discord category IDs |
discord_role_select | string|nil or string[] | Discord role IDs |
discord_guild_select | string|nil or string[] | Discord guild IDs |
object_list | table[] | 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:
| Name | Type | Default | Description |
|---|---|---|---|
| channelId | string | Discord channel ID for notifications | |
| notifyOnHeadshot | boolean | true | Send extra message for headshots |
| minDistanceForBonus | number | 200 | Minimum distance for bonus reward |
| bonusAmount | number | 50 | Currency 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:
Print debug output
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:
| Field | Type | Description |
|---|---|---|
pvp_kills | int | Total PvP kills |
last_pvp_kill | string|nil | Timestamp of last kill |
pvp_deaths | int | Total PvP deaths |
last_pvp_death | string|nil | Timestamp of last PvP death |
pvp_kd | float | Kill/death ratio |
deaths | int | Total deaths (all causes) |
kill_streak | int | Kills since last death |
death_streak | int | Deaths since last kill |
longest_kill | float | Longest kill distance in metres |
online_streak | int | Consecutive 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:
| Field | Type | Description |
|---|---|---|
id | int | Faction ID |
name | string | Faction name |
color | string|nil | Faction colour hex code |
image | string|nil | Faction image path |
balance | int|nil | Current faction treasury balance |
discord_role_id | string|nil | Linked 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
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):
| Filter | Type | Description |
|---|---|---|
group | string | Only players who are a member of this group (by name) |
whitelisted | bool | Only whitelisted / non-whitelisted players |
banned | bool | Only players who have an active ban restriction / players without one |
has_discord | bool | Only players with a linked Discord account |
has_balance_above | number | Only players whose balance is above this value |
has_balance_below | number | Only players whose balance is below this value |
last_online_after | string | Only players last seen after this time. Supports relative formats like "-7d", "-2h", "-1m", "-1y" or an absolute datetime string |
last_online_before | string | Only players last seen before this time. Same format as above |
in_faction | bool or string | true = in any faction, false = in no faction, or a faction name/id to match a specific faction |
variable | string or table | Only 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):
| Field | Type | Description |
|---|---|---|
id | int | Internal player ID |
username | string | Player username |
balance | int|nil | Current balance |
last_online | string|nil | Last seen timestamp (Y-m-d H:i:s) |
whitelisted | bool | Whether the player is whitelisted |
banned | bool | Whether 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:
| Field | Type | Description |
|---|---|---|
name | string | Variable name |
value | string|nil | Stored 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