Files
gbr-games/AUTHORING.md
T
gregandClaude Opus 5.5 04967a8a16 feat(builder): every boot through the node-agent, every lost file counted, Steam cache
What a builder boot sees is now what a node does:
- First boots (untouched, or -as-deployed with exactly a new server's
  settings) are deployed by the node-agent's BootTest in keep mode; the
  builder only watches the console and inspects what's left. The Go copy of
  the deploy (its own docker run, mounts and startup prefix) is gone. It had
  drifted: it gave the image's VOLUME folders their own volumes, which nodes
  don't, so saves there looked safe while every start loses them.
- Files written outside /server and /data are counted whatever their type
  (it only counted config-like extensions, so .sav or .db saves went
  unseen), including the image's VOLUME folders; logs, pid and lock files
  and system folders are left out.
- Boot tests ask the server to stay up holdSec (60 s, test.yaml can set it)
  after its ready line and to listen on every published container port
  (needs the node-agent's matching BootTest).
- Steam apps are downloaded once per build into a gbr.cache volume and copied
  into each boot, so later boots only check the files (STEAM_CACHE=off).
- RUNNER.md no longer suggests docker system prune, which deletes the
  gbr-boottest network when nothing is using it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-26 00:00:50 -04:00

116 lines
7.0 KiB
Markdown

# 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).
- 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
```bash
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.