Tooling

dotserve, the shell launcher that starts a dot-server without knowing how dot-server works, TMC's server tool (YAML configuration, games out of a directory, a browser client), and dot-ci, the shared CI every repository calls.

dot-server is deeply configurable — cvars, flags, server.cfg, autoexec.cfg, +command arguments, RCON, layered configuration from file, environment and argv. What it did not have was a front door. These are the doorbell.

dotserve

The one piece of the collection that is not a Godot addon: a shell script that needs bash and a Godot 4.7+ binary.

./install.sh                 # symlinks into ~/.local/bin
sudo ./install.sh --prefix /usr/local/bin
dotserve                            # sensible defaults, prints a join address
dotserve --game res://games/arena   # a specific game
dotserve --port 27015 --web         # also listen for browser clients
dotserve --restart                  # come back up after a crash
dotserve -- +sv_cheats 1 +map dm_arena

The first run writes a commented server.cfg under ~/.config/dotserve, generates an RCON password, and prints it once. Every run after that reads the file and never touches it.

What it does that is easy to leave out

It prints the address a friend can paste, including a LAN address — because “it says it started and my friend cannot connect” is almost always a bind address or a firewall, and the first thing that helps is knowing which address the server is actually on.

It refuses to start with a guessable RCON password. An empty one is fine and means the RCON listener does not open at all. What is refused is a placeholder that got shipped, copied out of a forum post, or left in a template.

The RCON password is never on a command line. server.cfg is written mode 600 before anything goes into it. argv and the environment are readable by every other process on the machine and both end up in pasted bug reports — the same reason DotConfig.sensitive_keys refuses secrets from them.

It never overwrites your server.cfg. Your edits are the server’s configuration.

Options worth knowing

--dry-run Print the command it would run, and exit.
--print-config Print the resolved configuration, and exit.
--install-service [NAME] Print a systemd unit for this invocation.
--restart Restart on a crash, with exponential backoff to a 60-second cap.
--web Also listen for browser clients, over WebSocket on the game port itself (TCP beside the native clients’ UDP). --ws-port is accepted from older unit files and ignored.

Exit codes are meaningful, so a supervisor can tell a misconfiguration from a crash: 2 usage, 3 no Godot, 4 no project, 5 bad config, 6 port in use. The generated systemd unit lists 3 to 6 under RestartPreventExitStatus.

TMC’s server tool

dot-server-deploy is where the collection comes together into something a server owner starts with one command. It is a Godot project that boots a dot-server, reads its configuration from cfg/*.yml, loads games out of content/, and serves a browser client — and it ships no game of its own. Every game, the lobby included, is a signed dot-cloud pack that the server installs from a content origin and every client downloads on connect, so the client is the same build whatever it joins and a new game needs no new one.

./setup.sh              # find a runtime, wire the addons, write ./server
./server                # start it

docker compose up -d    # or the same thing in a container

Windows: setup.bat (a shim for setup.ps1), then .\server.ps1.

It used to be called `dot-server-setup-test`

Same project, clearer name. If you have an older checkout or an older link, it is modcommunity/dot-server-deploy now.

Three scripts that are easy to confuse

./play.sh               # a server and the browser client, on the loopback
./play.sh playground    # start on a particular game
./play.sh games         # what else is in content/
./play.sh status
./play.sh down

play.sh is for a developer who changed a line and wants to look at it. It prints a link, rebuilds the export when it is out of date, copies embed.html — the page that takes ?server= from the query string — into the build beside Godot’s generated index.html, and tells you when a published pack in dist/ is older than the game repository it was built from. The addons are symlinked and always current; a pack is a snapshot, so it is the one part that goes stale, and a stale pack mounts cleanly, runs, and is last week’s game.

demo.sh is the other one: five servers, nginx terminating TLS, a public host, a separate registrable domain for the game and a sudo step to install a listener. Right for showing the platform to somebody, wrong for a one-line change.

Neither has anything to do with dev.sh, which is the website and starts no game at all. The two run side by side.

What it gives an operator

  • One command to start. setup.sh finds a Godot runtime, wires in the addons, writes a commented cfg/, and writes ./server. It never overwrites a config file that already exists. RCON ships on with a generated password, loopback only.
  • Configuration in YAML, split by subject — server.yml, net.yml, rcon.yml, auth.yml, groups.yml, permissions.yml, vote.yml, content.yml, security.yml, log.yml, replay.yml, party.yml, matchmaking.yml, admin_menu.yml, loading.yml. Anything dot-server exposes as a console variable can go in them under its own name.
  • Roles, not flags. dot-server’s permission model is flags, deliberately; groups.yml is the translation, so an operator writes permissions: [kick, ban, mute] under an admin group and a player gets the flags.
  • Games installed by name. --games g2gfast,playground (or TMC_GAMES) fetches each game’s descriptor and pack from the content origin at startup; owner/name@version names somebody else’s published game. A directory under content/ with a game.yml in it is a game, and changelevel switches between them with players still connected.
  • The players choose the next game. !game_nominate, !game_rtv, !game_vote, !game_timeleft, !game_next over the games in content/, through dot-vote. The prefix leaves the bare !rtv to a game that votes on its own maps. Each game gets its own time limit in its own game.yml.
  • A browser client. ./server export-web builds it; one export serves every server, because the address comes from ?server=.

The layout, and why it splits three ways

cfg/       what an operator edits
content/   the games it offers — a game.yml each; the game itself is a pack
data/      what the server writes — from_yaml.cfg, admins.json, bans.json, audit.jsonl, the pack cache
dist/      packs published from this box, and the index servers install from
host/      the boot: YAML -> DotServer
client/    the client shell. Contains no game; it mounts what the server names
web/       the browser build, and the page that takes ?server= from the URL

cfg/ is what you edit and data/ is what the server writes. content/ is written when the server installs a game, because the game.yml comes out of the signed pack; a box that carries its own content and installs nothing can mount it read-only.

`data/from_yaml.cfg` is what your YAML became

Read it when a setting seems to be ignored. The YAML is a surface over dot-server’s own vocabulary, not a second one, and that file is the translation.

It is also where every game in the collection is published as a pack and switched between under live players, and the first place anything in this collection was loaded in a browser.

dot-ci

What it is: the shared continuous integration for the whole collection, as three reusable GitHub Actions workflows (validate, release, release-maps) and a handful of shell scripts. Like dotserve it is not a Godot project: nothing in it is ever linked into one.

When you want it: you keep a Dot project in a public GitHub repository and want every push checked the way the collection is checked, and tagged releases packaged, without writing any of the logic yourself.

A repository calls it with about twenty lines:

name: CI
on:
  push:
    branches: [main]
  pull_request:
jobs:
  validate:
    uses: modcommunity/dot-ci/.github/workflows/validate.yml@v1

validate fetches the pinned Godot runtime, clones the addons your project lists in its .gitignore, runs the --import pass, parses every tracked script, runs every suite under a timeout, and reads each suite’s stderr even when it exits 0 — the same two steps as Installing. release runs on a v* tag, attaches the artifact and a SHA256SUMS file, and for a game builds its content pack as well. It signs nothing: a pack’s signature is made on a machine that holds the key (dot-cloud).