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]>
51 lines
4.4 KiB
Markdown
51 lines
4.4 KiB
Markdown
# 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 / `/readyup` can 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 with `g_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.txt` is 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.
|