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

4.4 KiB
Raw Blame History

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.