Files
gbr-games/AUTHORING.md
T
gregandClaude Opus 5.5 fe790ad223
Validate game templates / validate (push) Successful in 22s
Validate game templates / boot-test (push) Skipped
Validate game templates / publish (push) Skipped
fix(boot-test): run every boot test with exactly what a new server gets
CI's boot test gave every blank password-like setting a throwaway value. A
real deploy only fills x-generate fields, so a template whose server won't
start without one passed CI and failed for the first owner who deployed it.

boottest-payload now fills only x-generate fields, for CI and the builder
alike. The old behaviour is kept as -fill-blank-passwords, used only by
the builder's second boot to tell whether an empty setting is why a draft
fails. Messages and AUTHORING.md no longer say boot tests fill passwords;
a server that needs a join password gets x-generate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 22:01:16 -04:00

6.8 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.

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.
  • 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[@]}").
  • 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 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).

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.