Files
ql-lg-server/docs/HOW-IT-WORKS.md
T
esefandClaude Opus 5.5 bc5bd09c15 ql-lg-server: Quake Live LG vampiric practice server in Docker
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]>
2026-10-08 01:39:43 +03:00

51 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.