One-command setup (SteamCMD + minqlx build), .env settings with a listed/unlisted switch, three factories (lgvamp, lgvamp_nolag, lgvamp100), warmup-text and welcome minqlx plugins, watchdog/backup/daily-restart, docs for admins and players. Co-Authored-By: Claude Opus 5.5 <[email protected]>
4.4 KiB
How it works (and why)
Notes for anyone changing this setup. Each point below was hit while building it.
Permanent warmup instead of a match
Real LG duel servers (x76) report PRE_GAME (warmup) even with 9 players on: everyone plays FFA in warmup, which never ends. That's exactly what LG practice needs: no countdown, no score resets, no map reloads.
sv_warmupReadyPercentage 2: a match would need 200% of players ready, so F3 //readyupcan never start one. This cvar is locked, so it can only be set in the factory, not in server.cfg.g_warmupReadyDelay 0: no auto-start timer.
Approaches that don't work:
g_doWarmup 0(always in a match): QL forfeits a running match when fewer than 2 players are on, even withg_allowForfeit 0. With one player you get reload → countdown → a few seconds of play → forfeit, every 15–20 s. A permanent bot (bot_minplayers 2) hides that, but players find it weird.- Auto-start (
sv_warmupReadyPercentage 0+ a delay): a countdown and score reset every time a second player joins.
fraglimit 50, timelimit 15, g_overtime 0 are x76's values; they only matter if a match ever starts.
The warmup text (minqlx plugin nowarmuptext)
In warmup the client draws "The match will begin when more players join" / "Press F3 to ready yourself". No cvar hides it. The client draws it only while configstring CS_WARMUP (index 5) holds a time (warmup sets \time\-1). The plugin hooks set_configstring and sends an empty value for index 5 instead. The server itself stays in warmup. The first map is set up before plugins load, so the plugin also clears the value once when it loads. Check: docker logs ql-lg | grep nowarmuptext shows CS_WARMUP=''.
minqlx is started with only our plugins (qlx_plugins "nowarmuptext,welcome"). Its default plugin list needs a Redis server, which this setup doesn't run. QL's built-in access.txt covers admins and bans without it.
Vampiric, measured
g_vampiricDamage 1 heals exactly 1:1. Measured with g_debugVampiricDamage 1 and two bots: an LG hit does 6 damage and the attacker gains 6. When both players hit each other at the same time, neither health changes. Spawning with 200 HP (g_startingHealthBonus 100) means health above 100 decays ~1/s, which is where the "I'm losing health" feeling comes from. lgvamp100 removes it. (The stock _training factory uses 0.75.)
Antilag
g_lagHax 1, g_lagHaxMs 240, g_lagHaxMsec 10, g_lagHaxHistory 12: hits are checked against where the target was on the shooter's screen, for pings up to 240 ms. lgvamp_nolag sets g_lagHax 0.
The RCON password leak
qzeroded's built-in ZMQ auth logger (CZMQ zauth) prints zauth: ... password=<your rcon password> in plain text on every RCON login. The compose command pipes the server's output through grep -v zauth: so those lines never reach docker logs. TINI_KILL_PROCESS_GROUP=1 makes stop and restart signals reach the server through that pipe. If the server dies, grep sees end-of-file, the container exits and Docker restarts it.
Two more places the password shows up: home/baseq3/zmqpass.txt (QL writes it on every start) and the output of cvarlist. tools/qlrcon.py filters it out of anything it prints.
Unlisted servers answer no queries
With sv_master 0, QL ignores getstatus, getinfo and Steam A2S queries (sv_serverType doesn't change that). So the health check (tools/qlq.py) uses the connect handshake: send getchallenge and the reply is print "No Steam auth token.". That works for listed and unlisted servers. Player info comes from RCON status, where bots have the address bot; that's how the daily restart can tell whether a human is on.
Small things
- SteamCMD needs
+@sSteamCmdForcePlatformType linux, or it fails with "Missing configuration". - minqlx must be built against the same Python as the image (Debian bookworm, 3.11). setup.sh builds it in a bookworm container, pinned to a tested commit.
- The minqlx start script turns on
zmq_stats_enable(TCP, all interfaces). The compose command turns it off again. - A tied FFA never ends on the timelimit (it goes to overtime); irrelevant in permanent warmup.
- Cvars changed over RCON survive map changes until the container restarts.
home/baseq3/access.txtis rewritten by QL on map load/shutdown (see ADMIN.md).- Polling RCON from several clients at once while the server restarts once coincided with a crash. Poll from one client, slowly.