Files
gbr-games/AUTHORING.md
T
gregandClaude Opus 5.5 929cbc31cf
Validate game templates / validate (push) Successful in 19s
Validate game templates / boot-test (push) Skipped
Validate game templates / publish (push) Skipped
fix(builder): keep every game's name and code out of the builder
The builder must be game-agnostic, but its prompts named games: AUTHORING.md
(the model's rules) used three games as examples, the research prompt listed
two games' editions, the schema described containerUser by one game's image,
and every draft prompt included two real templates from the repo, so the
model saw, and could copy, other games' commands and options.

- AUTHORING.md, the research prompt and the schema describe the rules without
  naming a game.
- The draft prompt's format example is now one invented server (no real
  command, options, ports or app id), checked by
  TestExampleTemplateFollowsTheRules against validation and the draft checks.
- Comments, help texts, BUILDER.md and test fixtures use invented names and
  values.
- TestBuilderIsGameAgnostic reads every game's name and id from games/ and
  fails if any appears in the builder's code, tests, rules, schemas or docs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 17:58:27 -04:00

6.6 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}.
  • 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.