Reference
Writing game definitions
A game definition is a YAML file (game.yaml) that describes one game completely: platforms,
install and update steps, ports, variables, processes, config forms, RCON, status query, file
manager and backup rules, and hooks. NGCP renders it into concrete command lines, files and
firewall rules for each service.
The format is checked strictly: unknown keys and wrong types are errors with line numbers, and
every problem is reported at once. The full JSON Schema is published as
docs/gamedef.schema.json in the source repository (ngcp-controller gamedef-schema prints
it). Point your editor’s YAML plugin at it for completion and checks:
# yaml-language-server: $schema=./gamedef.schema.json
The shipped definitions (Shipped games) are good examples; ArmA 3 uses nearly every feature.
Uploading a definition
- Open Games and choose Write a new definition, or open an existing game.
- Paste or edit the YAML. Check for problems validates it without saving.
- Save. Each save needs a higher
versionthan the last one (“The version must go up”).
Rules:
- Shipped definitions are built-in. Saving one in the panel turns it into a custom definition, which upgrades never overwrite.
- Every saved version is kept; Compare with an older version shows a diff.
- Offered for new services hides or shows a game in the create dialog without touching existing services.
- Built-in definitions cannot be deleted; custom ones only when no service uses them.
- Changes reach services the next time their spec is sent (at once for connected nodes). Install steps only run on install, reinstall or update.
- Limits: 256 KiB of YAML, 64 KiB per script. Managing definitions needs
games:write.
Through the API, POST /api/v1/game-definitions takes {"source_yaml": "...", "scripts": {"scripts/setup.sh": "..."}}; POST /api/v1/game-definitions/validate checks without saving.
A minimal definition
id: mygame
version: 1
name: My Game
category: fps
platforms: [windows, linux]
slots: {min: 1, max: 32, default: 16}
install:
windows:
- {type: steamcmd, name: My Game server, app_id: 123456, login: anonymous, validate: true}
linux:
- {type: steamcmd, name: My Game server, app_id: 123456, login: anonymous, validate: true, executable: [server.x86_64]}
ports:
- {role: game, proto: udp, default: 27015, offset: 0}
- {role: query, proto: udp, default: 27016, offset: 1}
variables:
- {key: server_name, label: Server name, type: string, default: "My server", user_editable: true}
processes:
- role: main
windows: {exe: server.exe}
linux: {exe: server.x86_64}
args: '-port {port.game} -queryport {port.query} -maxplayers {slots} "-name={var.server_name}"'
stop: {stdin: quit, timeout_sec: 30}
ready_regex: "Server started"
query: {type: a2s, port: "{port.query}"}
Placeholders
Templates (command-line arguments, file contents, paths, URLs, config binds) use placeholders in curly braces:
| Placeholder | Value | Where |
|---|---|---|
{var.<key>} |
A variable’s value | Everywhere |
{port.<role>} |
The port allocated for that role | Everywhere |
{slots} |
The service’s player slots | Everywhere |
{install_dir} |
The service’s install directory (absolute) | Everywhere |
{ip} |
The IP the service binds (the node’s primary IP, or 0.0.0.0) |
Everywhere |
{os} |
windows or linux |
Everywhere |
{index} |
The process’s number within its role (headless clients 1..N) | Processes only |
{agent_exe} |
The agent’s own executable (for built-in helpers) | Processes only |
Every placeholder must exist: a {var.x} without a variable x or a {port.y} without a port
role y is an error. {cfg.*} placeholders are not supported; bind a config setting to a
variable instead.
Command lines are split into arguments before placeholders are filled in, like a shell
would split them. A placeholder’s value is therefore always exactly one argument, whatever it
contains. Quote an argument that has a fixed prefix so it stays together:
"-mod={var.mods}" is one argument even when empty.
Scripts and hooks also get the values as environment variables: NGCP_VAR_<KEY>,
NGCP_PORT_<ROLE>, NGCP_INSTALL_DIR, NGCP_SLOTS, NGCP_IP, NGCP_OS, NGCP_SERVICE_ID.
Top-level keys
id, version, name, category, icon
id: arma3 # lower case, digits and _, starting with a letter or digit; never change it
version: 3 # raise on every change
name: ArmA 3
category: fps # free text, for grouping
icon: "" # optional
platforms
[windows], [linux] or [windows, linux]. Nodes of other OSes are not offered.
slots
slots: {min: 2, max: 256, default: 64}
# or the shorthand: default_slots: 64
ports
ports:
- {role: game, proto: udp, default: 2302, offset: 0}
- {role: query, proto: udp, default: 2303, offset: 1}
- {role: rcon, proto: tcp, default: 2307, offset: 5}
| Key | Meaning |
|---|---|
role |
Name used in {port.<role>} (lower case). |
proto |
tcp, udp or both. |
offset |
Distance from the service’s base port (0-100). Ports are allocated as one block. |
default |
The game’s usual port. Its block is used when it fits inside the node’s port range and is free. |
Reserve offsets the game uses implicitly (ArmA’s +3 voice port) as roles, so no other service gets them. The firewall opens all of a service’s ports.
variables
variables:
- {key: server_name, label: Server name, type: string, default: "My server", user_editable: true}
- {key: admin_password, label: Admin password, type: password, generate: true, user_editable: true}
- {key: difficulty, label: Difficulty, type: select, options: [easy, normal, hard], default: normal, user_editable: true}
- {key: headless_clients, label: Headless clients, type: int, default: 0, min: 0, max: 8, user_editable: true}
- {key: accept_eula, label: I accept the EULA, type: bool, default: false, user_editable: true}
- {key: mods, label: Mods, type: modlist, default: "", user_editable: true}
| Key | Meaning |
|---|---|
key |
Name used in {var.<key>} (lower case). |
type |
string, int (with min / max), bool, password, select (with options), modlist. |
default |
Default value. |
user_editable |
Service owners may change it. Otherwise only administrators. |
generate |
Create a random value when the service is created (for passwords). |
admin_only |
Only SuperAdmins and Admins may set it; a package made by an administrator can preset it. |
required |
A game server cannot be saved without a value (0 counts as none). |
rel_path |
The value must be a path inside the install directory. |
command_line |
Used as a whole word of args ("{var.arguments}"), the value is split into arguments and may use the same placeholders. |
label, description |
Shown in the panel. |
password values are sealed per service and never returned by the API. A modlist holds
Workshop item ids; in templates it renders as the mod folders joined with ;
(mods/@cba_a3;mods/@ace). Values are limited in length and cannot contain control characters.
processes
processes:
- role: main
windows: {exe: arma3server_x64.exe}
linux: {exe: arma3server_x64}
args: >-
-port={port.game} -config=server.cfg "-mod={var.mods}"
env: {SteamAppId: "892970"}
console: {mode: pipe, log_files: ["profiles/*.rpt"], tail_log_files: true}
stop: {rcon: "#shutdown", timeout_sec: 15}
ready_regex: "Connected to Steam servers"
- role: headless
count: "{var.headless_clients}"
windows: {exe: arma3server_x64.exe}
linux: {exe: arma3server_x64}
args: -client -connect=127.0.0.1 -port={port.game} -profiles=hc{index} -name=hc{index}
start_delay_sec: 15
restart_with_main: true
| Key | Meaning |
|---|---|
role |
main (exactly one, runs once), headless or sidecar. |
count |
Template for how many to run (not for main); 0 runs none. |
windows.exe, linux.exe |
Executable relative to the install directory, per OS. |
args |
Command-line template (split into arguments first, see above). |
env |
Extra environment variables (templates allowed). |
console.mode |
pipe: the process’s output is the console. |
console.log_files |
Globs of log files to show in the console too (relative paths). |
console.tail_log_files |
Follow those files as they grow. |
console.clear_logs_on_start |
Clear them at each start. |
stop.stdin |
Command written to the console on stop (for example quit, stop). |
stop.rcon |
RCON command sent on stop (needs an rcon section), for example #shutdown. |
stop.timeout_sec |
How long to wait before terminating (0-600; default 10). |
ready_regex |
A console line that means “running”; until it appears the service is starting. If it never appears, the service is assumed to be up after a while. |
start_delay_sec |
Wait this long after the main process before starting (0-600). |
restart_with_main |
Restart this process whenever the main one restarts. |
install and update
Steps per OS, run in order. update has the same shape; without update, an update re-runs the
install’s SteamCMD steps without validation.
install:
windows:
- {type: steamcmd, name: Server files, app_id: 233780, login: account, validate: true}
- {type: http, name: Java runtime, resolver: adoptium, version: "21", dest: jre, extract: auto, strip_components: 1}
- {type: http, name: Server jar, url: "https://example.com/server-1.2.jar", sha256: "<64 hex>", dest: server.jar, extract: none}
- {type: git, name: Scripts, repo: "https://github.com/example/server-scripts", ref: main, dest: scripts}
- {type: script, name: Prepare, run: "scripts/prepare.{ps1|sh}"}
- {type: file, name: Default config, dest: server.cfg, content: "hostname = \"{var.server_name}\";\n"}
linux:
- {type: steamcmd, name: Server files, app_id: 233780, login: account, validate: true, executable: [server_x64]}
Common keys on every step: type, name (shown in progress), when (a condition, below) and
executable (Linux: files to mark executable afterwards).
| Type | Keys |
|---|---|
steamcmd |
app_id, login (anonymous or account = the vault account), validate, beta, beta_password, force_platform (windows / linux: download another platform’s depot) |
http |
url (HTTPS only) with sha256, or resolver (papermc, mojang, adoptium) with version (latest = newest); dest; extract (auto, none, zip, tar.gz); strip_components (0-5) |
git |
repo (HTTPS URL), ref, dest. The node needs git. |
script |
run: a path under scripts/; x.{ps1|sh} picks .ps1 on Windows and .sh on Linux. env: extra variables. |
file |
dest, content (a template), overwrite (default false: an existing file is kept, so user edits survive). |
Downloads are verified before anything is unpacked; archives cannot write outside the install directory. git and scripts run as the service account.
Conditions: when: "{var.flavor} == paper" (or !=) skips a step unless it holds. The
Minecraft definition uses this to offer Paper and Vanilla in one definition.
A steamcmd step (and the workshop section) can take its id and login from variables instead: app_id_var names an int variable and login_var a select variable with the options anonymous and account. The shipped steam_server definition works this way.
preserve_on_reinstall
Globs (relative, ** allowed) that a reinstall keeps:
preserve_on_reinstall: [server.cfg, "profiles/**", "mpmissions/**"]
workshop
workshop:
app_id: 107410 # the game's Workshop app id (not the server's)
login: account # anonymous or account
install_to: mods # folder for the mods
name_strategy: "@{title_slug}" # folder name: {title_slug} and/or {id}
keys_dir: keys # copy *.bikey files here
lowercase_on_linux: true
config_files
Forms for the Configs tab:
config_files:
- path: server.cfg
label: Server
format: arma_cfg
settings:
- {key: hostname, label: Server name, type: string, bind: "{var.server_name}"}
- {key: maxPlayers, label: Max players, type: int, bind: "{slots}", min: 2, max: 256}
- {key: BattlEye, label: BattlEye anti-cheat, type: bool, default: true, user_editable: false}
- {key: vonCodecQuality, label: Voice quality, type: slider, min: 0, max: 30, step: 1, default: 10}
- {key: "motd[]", label: Message of the day, type: list}
- path: battleye/launch/battleye/beserver_x64.cfg
label: BattlEye RCon
format: keyvalue_space
platforms: [linux]
settings:
- {key: RConPassword, label: RCon password, type: password, bind: "{var.be_password}", user_editable: false}
| File key | Meaning |
|---|---|
path |
Relative to the install directory (templates allowed). |
format |
ini, keyvalue, keyvalue_space, properties, env, json, yaml, xml, arma_cfg, regex. |
label |
Card title. |
platforms |
Only on these OSes. |
| Setting key | Meaning |
|---|---|
key |
The setting’s name in the file. INI: Key or [Section]Key (a repeated key is a list). key=value formats: key. arma_cfg: hostname, arrays motd[], nested classes with dots (Missions.Mission1.template). JSON / YAML: a dot path (server.maxPlayers). XML: Root/Child/Leaf, or Root/Child@attr for an attribute. |
type |
string, text, int, float, bool, password, select, slider, list. |
bind |
Keep the setting equal to this template. {var.x} or {slots} alone: the form edits that value. Anything else: read-only. |
default |
Shown when the key is not in the file. |
options, min, max, step |
For selects, numbers and sliders. |
true_value, false_value |
How a bool is spelled (default 1 / 0 for arma_cfg, true / false elsewhere). |
user_editable |
false: only administrators change it. |
pattern |
For regex files: a regular expression whose first group is the value. |
label, description |
Shown in the form. |
rcon
rcon: {type: battleye, port: "{port.rcon}", password: "{var.be_password}"}
type: source, battleye, minecraft or ts3query; user for ts3query (default
serveradmin). The agent always connects to 127.0.0.1; a host key is accepted for
compatibility but not used. Make sure the game’s own config enables RCON with the same port and
password (bind them in config_files).
query
query: {type: a2s, port: "{port.query}"}
type: a2s, minecraft or generic (TCP connect only). See RCON and player
counts.
file_manager
file_manager:
deny: ["*.exe", "*.dll", "*.so", "battleye/**", ".ngcp/**"]
Globs hidden from non-administrators in the file manager and SFTP.
backup
backup:
include: ["saves/**"] # empty or absent: everything
exclude: ["*.log", "mods/**"]
hooks
Scripts under scripts/ run as the service account at points in the lifecycle:
hooks:
after_install: scripts/after_install.{ps1|sh}
before_start: scripts/before_start.{ps1|sh}
before_backup: scripts/save_world.{ps1|sh}
| Hook | Runs | If it fails |
|---|---|---|
after_install |
After a successful install | The install fails |
before_start |
Before every start; stopped after 5 minutes | The service stays stopped; the console says why |
before_backup |
Before every backup (for example a save command); stopped after 5 minutes | The backup runs anyway; the console says why |
Scripts
Scripts live next to game.yaml under scripts/ (shipped definitions) or are uploaded with
the definition (scripts in the API). Each referenced script must exist for every platform the
game supports. On Windows they run with PowerShell, on Linux with bash, as the service account,
in the install directory, with the NGCP_* environment variables.
Checking your work
- Check for problems in the editor, or
POST /api/v1/game-definitions/validate. - Create a test service on each OS, install it, start it and check the console, the ports on the overview, the config forms and a backup.
- Raise
versionwith every change you want nodes to pick up.