150 lines
10 KiB
Markdown
150 lines
10 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.
|
|
|
|
### The image: SteamCMD first
|
|
- If the game's dedicated server is on Steam and ships a Linux build, the template is
|
|
`defaultDockerImage: cm2network/steamcmd` with `installMethod: STEAMCMD` and the **server's** app id
|
|
(not the game's). Never a community image of that server (`wolveix/...`, `lloesche/...` and the like):
|
|
they keep game files and saves in their own folders (`/config`, `/home/<user>`), outside `/server`, so
|
|
everything is lost on the next start, and no setting can move them.
|
|
- Community images are only for servers that aren't on Steam for Linux: Windows-only servers that need
|
|
Proton or Wine, and games not on Steam.
|
|
- Many Linux servers save under `$HOME` (Unreal Engine games: `~/.config/Epic/<Game>/Saved`). In
|
|
`cm2network/steamcmd`, `$HOME` is `/home/steam`, outside the volume. Start the command with
|
|
`export HOME=/server;` so those saves land in the volume, and list them in `backupPaths` relative to
|
|
`/server` (e.g. `.config/Epic/<Game>/Saved/SaveGames`).
|
|
|
|
### 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.
|
|
- A port the game uses over both protocols is listed twice: `"7777/udp"` and `"7777/tcp"`.
|
|
- A game that tells players a second port (a reliable-messaging or query port it announces) needs it as
|
|
`{PUBLIC_PORT_<port>}` passed to the game's "external port" option, since nodes may publish it elsewhere.
|
|
- 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 setting must reach the server.** Settings only arrive as environment variables (and in
|
|
`configFilePath`, if set). A server that doesn't read environment variables (most official servers,
|
|
every Unreal Engine server) ignores a setting unless the template delivers it: in `configFilePath`, as a
|
|
launch option the startup command builds from the variable, or written into the server's own config
|
|
file by the startup command before it starts. A setting that nothing reads looks editable in the panel
|
|
and does nothing: leave it out instead.
|
|
- `configFilePath` writes **every** setting into **one** file. When a server reads its settings from
|
|
several files (Unreal: `Game.ini`, `Engine.ini`, `GameUserSettings.ini` under
|
|
`<Game>/Saved/Config/LinuxServer/`), leave `configFilePath` out and write each file from the startup
|
|
command before the server starts:
|
|
`printf '[/Script/Engine.GameSession]\nMaxPlayers=%s\n' "${MAX_PLAYERS:-4}" > "$cfg/Game.ini";`.
|
|
The server may rewrite these files when it stops; writing them on every start keeps the panel in charge.
|
|
- Never carry over a community image's variable names (`MAXPLAYERS`, `AUTOSAVENUM`, `PUID`, `SKIPUPDATE`...)
|
|
into a SteamCMD template: only that image's own scripts read them. Use the game's real setting names.
|
|
- 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.
|