10 KiB
10 KiB
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
/serverand/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:1000unless the template setscontainerUser. 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 beforeP),bIsPvP→B_IS_PV_P. - If
configFilePathis set, the same settings are also written to that file inconfigFileFormatbefore each start. defaultStartupCommand, when set, runs asbash -c "… cd /server && <command>"; otherwise the image's own entrypoint runs.installMethod: STEAMCMDinstallsinstallConfig.appIdinto/serverwith 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:
defaultConfigvalue"", and theconfigSchemaproperty 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.yamlundersecrets, 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/steamcmdwithinstallMethod: STEAMCMDand 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). Incm2network/steamcmd,$HOMEis/home/steam, outside the volume. Start the command withexport HOME=/server;so those saves land in the volume, and list them inbackupPathsrelative to/server(e.g..config/Epic/<Game>/Saved/SaveGames).
Ports
defaultPortslists 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 declaresUSER, or locks a home folder to a uid other than 1000), setcontainerUserto thatuid: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 isRCONPortsilently ignores the owner's value; it arrives asRCONPORT. - 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 $ARGSsplits "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 frominstallMethodorinstallConfig.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
requiredMemoryMbis the container's hard limit. A JVM or engine heap must stay about 1 GB below it: a3Gheap for4096MB. 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
defaultConfighas aconfigSchemaproperty, 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: inconfigFilePath, 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. configFilePathwrites every setting into one file. When a server reads its settings from several files (Unreal:Game.ini,Engine.ini,GameUserSettings.iniunder<Game>/Saved/Config/LinuxServer/), leaveconfigFilePathout 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 shortdescriptionfor a player, atype, adefaultequal to thedefaultConfigvalue, and acategory: one ofSecurity & 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 defaultfalse. Nevertrue; 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 toGBRHosting - {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 intest.yamlasreadyExample; 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
readyPatternout, and the boot test checks the server stays up (settleSecintest.yaml, default 120 s). - After its ready line the server must stay up for
holdSec(default 60 s), and then listen on every port indefaultPorts. A server that binds other ports, or only127.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.timeoutSeccovers 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/rconPasswordKeypointing at thex-generatesetting). If a game has neither, mark the action"type": "UNSUPPORTED"; never invent one. playerJoinRegex/playerLeaveRegexcapture the player name from real log lines.
Before opening a PR
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.