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>
116 lines
7.0 KiB
Markdown
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.
|