# Writing a game template House rules for `games//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 ` 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 && "`; 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/`), 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//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//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_}` 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_}`. ### The container user - Run `gbrgames inspect-image -game `. 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 ` 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 `/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_}`, `{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 go run ./cmd/gbrgames first-boot -game # 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.