Files
gbr-games/AUTHORING.md
T
Greg Reed 1902c62366
Validate game templates / validate (push) Successful in 20s
Validate game templates / boot-test (push) Successful in 3m49s
Validate game templates / publish (push) Skipped
fix(builder): SteamCMD is the only option for Linux Steam servers; settings must reach the server
2026-11-07 02:16:19 +00:00

10 KiB

Writing a game template

House rules for games/<id>/game.yaml and test.yaml. They apply to people and to the AI builder alike; the builder reads this file as its instructions. Most rules exist because breaking them broke a real server. gbrgames validate and gbrgames inspect-image -game <id> enforce what they can.

How a server actually runs

Know this before writing anything:

  • The node-agent creates one Docker volume per server and mounts it at both /server and /data. Game files, saves and config live there. Anything written elsewhere is lost when the container is recreated, which happens on every start.
  • The container runs as 1000:1000 unless the template sets containerUser. Never as root.
  • Every key in defaultConfig (merged with the owner's changes) reaches the container as an environment variable, named by the agent's rule: insert _ before an upper-case run that follows a lower-case letter, then upper-case everything. ServerName → SERVER_NAME, MaxPlayers → MAX_PLAYERS, RCONPort → RCONPORT (no underscore: no lower-case letter before P), bIsPvP → B_IS_PV_P.
  • If configFilePath is set, the same settings are also written to that file in configFileFormat before each start.
  • defaultStartupCommand, when set, runs as bash -c "… cd /server && <command>"; otherwise the image's own entrypoint runs.
  • installMethod: STEAMCMD installs installConfig.appId into /server with anonymous SteamCMD before the server starts.
  • Nodes reach a server's RCON, REST and query ports over the Docker network, never through published ports.

Rules

Passwords and secrets

  • Admin, RCON and REST passwords: defaultConfig value "", and the configSchema property gets "x-generate": {"type": "password"}. The panel then gives every server its own password. Never put a placeholder or default password in a template. Shared defaults are readable by anyone from the public game list.
  • Join passwords stay "", with "x-savedValue": {"key": "GAME_JOIN_PASSWORD", "label": "Game join password", "secret": true}. If the server can't start without one, give it "x-generate": {"type": "password"} as well, so every new server gets its own. Boot tests run with exactly what a new server gets: empty settings stay empty.
  • No real credentials, tokens or IDs anywhere in this repo. A value a server can't start without (a player ID, a license token) goes in test.yaml under secrets, mapped to a CI secret name.

The image: SteamCMD first

  • If the game's dedicated server is on Steam and ships a Linux build, the template is defaultDockerImage: cm2network/steamcmd with installMethod: STEAMCMD and the server's app id (not the game's). Never a community image of that server (wolveix/..., lloesche/... and the like): they keep game files and saves in their own folders (/config, /home/<user>), outside /server, so everything is lost on the next start, and no setting can move them.
  • Community images are only for servers that aren't on Steam for Linux: Windows-only servers that need Proton or Wine, and games not on Steam.
  • Many Linux servers save under $HOME (Unreal Engine games: ~/.config/Epic/<Game>/Saved). In cm2network/steamcmd, $HOME is /home/steam, outside the volume. Start the command with export HOME=/server; so those saves land in the volume, and list them in backupPaths relative to /server (e.g. .config/Epic/<Game>/Saved/SaveGames).

Ports

  • defaultPorts lists only what players connect to (game and query ports). RCON, REST and admin ports are never published.
  • Use "7777/udp" or "8211:8211/udp". Every port needs its protocol.
  • A port the game uses over both protocols is listed twice: "7777/udp" and "7777/tcp".
  • A game that tells players a second port (a reliable-messaging or query port it announces) needs it as {PUBLIC_PORT_<port>} passed to the game's "external port" option, since nodes may publish it elsewhere.
  • Nodes always publish these container ports (only the host side moves when servers share a node). So the game's own port is not a setting: write it into the startup command or config file. A game that must announce the port players use gets {PUBLIC_PORT_<port>}.

The container user

  • Run gbrgames inspect-image -game <id>. If the image is built around its own user (it declares USER, or locks a home folder to a uid other than 1000), set containerUser to that uid:gid, as the inspector reports it.

The startup command

  • Read settings under the names the agent gives them (see above). ${RCON_PORT} when the setting is RCONPort silently ignores the owner's value; it arrives as RCONPORT.
  • Use ${VAR:-default} for anything that may be unset.
  • Only use launch options the game's documentation lists, spelled as it spells them. Servers ignore options they don't know, so an invented one boots fine and its setting does nothing.
  • Quote text settings where they're used: -name "${SERVER_NAME}". ARGS="-name ${SERVER_NAME}"; ./server $ARGS splits "GBRHosting - My Server" into words; build optional arguments in a bash array instead (args=(-name "$SERVER_NAME"); [ -n "$PASSWORD" ] && args+=(-password "$PASSWORD"); ./server "${args[@]}").
  • Templates run on players' own machines. The startup command and install never pipe a download into a shell, decode hidden (base64) content, use eval, raw network tools or /dev/tcp, become another user, or touch the machine (/etc, cron, systemd, firewall). Files a server needs come from installMethod or installConfig.files, from the game's official source; every download address is listed for review.
  • If the image installs the game itself on start, make sure a failed install is retried rather than starting a server that isn't there: log in to SteamCMD once (+login anonymous +quit) before the install, and retry the install a few times until the server's executable exists.

Memory

  • requiredMemoryMb is the container's hard limit. A JVM or engine heap must stay about 1 GB below it: a 3G heap for 4096 MB. A heap equal to the limit is killed on start.

Settings (defaultConfig and configSchema)

  • Take keys and default values from what the server really writes: run gbrgames first-boot -game <id> and read the settings in its report.
  • Every key in defaultConfig has a configSchema property, and the other way round.
  • Every setting must reach the server. Settings only arrive as environment variables (and in configFilePath, if set). A server that doesn't read environment variables (most official servers, every Unreal Engine server) ignores a setting unless the template delivers it: in configFilePath, as a launch option the startup command builds from the variable, or written into the server's own config file by the startup command before it starts. A setting that nothing reads looks editable in the panel and does nothing: leave it out instead.
  • configFilePath writes every setting into one file. When a server reads its settings from several files (Unreal: Game.ini, Engine.ini, GameUserSettings.ini under <Game>/Saved/Config/LinuxServer/), leave configFilePath out and write each file from the startup command before the server starts: printf '[/Script/Engine.GameSession]\nMaxPlayers=%s\n' "${MAX_PLAYERS:-4}" > "$cfg/Game.ini";. The server may rewrite these files when it stops; writing them on every start keeps the panel in charge.
  • Never carry over a community image's variable names (MAXPLAYERS, AUTOSAVENUM, PUID, SKIPUPDATE...) into a SteamCMD template: only that image's own scripts read them. Use the game's real setting names.
  • Every property has a title, a short description for a player, a type, a default equal to the defaultConfig value, and a category: one of Security & Access, Gameplay & Rules, World & Environment, System & General. Mark the few that matter when deploying with "showOnDeploy": true.
  • An EULA or other agreement is a boolean with "x-consent": {"label": "...", "url": "..."} and default false. Never true; the owner accepts it themselves.
  • Keys may only use letters, digits, _, - and ..
  • Placeholders the panel fills in: {SERVER_NAME}, {PUBLIC_IP}, {PUBLIC_HOST}, {PUBLIC_PORT_<port>}, {saved:KEY}. Server names default to GBRHosting - {SERVER_NAME}.

Being ready

  • readyPattern: a regex that matches the line the server prints once players can join. Take it from a first boot (its report suggests one), and put that real line in test.yaml as readyExample; validation checks the pattern matches it.
  • failPatterns: lines that mean the server will never come up (e.g. an unaccepted EULA).
  • No reliable ready line: leave readyPattern out, and the boot test checks the server stays up (settleSec in test.yaml, default 120 s).
  • After its ready line the server must stay up for holdSec (default 60 s), and then listen on every port in defaultPorts. A server that binds other ports, or only 127.0.0.1, fails: players can't reach it.

test.yaml

  • config: only what the server can't start without in CI (accepting the EULA, for example).
  • secrets: required values that can't be public, mapped to CI secret names.
  • timeoutSec covers install plus start; SteamCMD games need 2400 or more.

Commands and players (agnosticCommandConfig)

  • Kick, ban, save and broadcast go through RCON or REST using the server's own password key (passwordKey / rconPasswordKey pointing at the x-generate setting). If a game has neither, mark the action "type": "UNSUPPORTED"; never invent one.
  • playerJoinRegex / playerLeaveRegex capture the player name from real log lines.

Before opening a PR

go run ./cmd/gbrgames validate
go run ./cmd/gbrgames inspect-image -game <id>
go run ./cmd/gbrgames first-boot -game <id>     # on a machine with Docker and the gbr-boottest network

CI then boot-tests the changed game with the node-agent's real deploy code before anything reaches the panel.