- The drafter no longer grows one conversation. Each request is the rules and facts, the model's latest template, what is wrong now and one line per earlier round, so long builds can't push the facts out of the model's 32K context (Ollama drops the oldest part silently). The image's scripts in the facts are capped at 24000 characters in all, and diagnosis check results at 16000. - Every model call logs its prompt and answer tokens, with a warning near the context limit. - unsafeCommands: a draft whose startup command or installConfig pipes a download into a shell, decodes base64, uses eval, raw network tools or /dev/tcp, sudo/su, setuid bits, writes /etc, touches cron/systemd/firewall, or looks like a miner goes back to the model. AUTHORING.md has the rule. - The PR summary lists every address nodes would download from under "Review carefully", marks the measured checks as measured, and labels the research notes as the model's reading, not checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
7.5 KiB
Markdown
120 lines
7.5 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[@]}"`).
|
|
- 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 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.
|