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]>
This commit is contained in:
esefandClaude Opus 5.5 committed 2026-10-08 01:39:43 +03:00
commit bc5bd09c15
25 files changed
+1009

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
# Running the server
Everything below is run from the repo folder on the server machine.
## Console (RCON)
`./rcon <command>` runs one server command; `./rcon` alone opens a `ql>` prompt (Ctrl+D quits). It runs inside the container and reads `secrets/rcon-password.txt` itself, so you never type the password. RCON listens on 127.0.0.1 only, so nobody can reach it from outside the machine.
| Command | |
|---|---|
| `status` | players with their slot number (`num`), ping, address (`bot` = bot) |
| `say <text>` | message to everyone |
| `addbot hunter 5` | add a bot (names: hunter, anarki, hossman, …; skill 1–5) |
| `kick allbots` | remove all bots (prints a harmless "Player allbots is not on the server") |
| `clientkick <num>` | kick a player |
| `mute <num>` / `unmute <num>` | chat mute |
| `tempban <num>` | kick + ban until the map changes |
| `ban <num>` / `unban <num>` | permanent ban (stored in `access.txt`) |
| `addadmin <num>` / `addmod <num>` | give a connected player admin / mod rights |
| `reload_access` | re-read `access.txt` |
| `map thunderstruck lgvamp` | reload the map (or switch factory: `lgvamp_nolag`, `lgvamp100`) |
**Careful:** `cvarlist` prints the RCON password into the server log. `./rcon` hides it from you, but avoid running it anyway.
## Admins and bans: `home/baseq3/access.txt`
One line per player: their 64-bit Steam ID, a `|`, and `admin`, `mod` or `ban`:
```
76561198000000000|admin
76561198000000001|ban
```
Quake Live writes this file back from memory on every map load and on shutdown, so a hand edit can be overwritten. Either use the console commands above (`addadmin`, `ban`, `unban`), or edit the file and run `./rcon reload_access` right away. To unban someone who isn't connected, delete their line and run `reload_access`.
## Settings changes
- `.env` (name, password, listed, factory, welcome text, ports): edit it, then run `docker compose up -d`.
- Gameplay: edit `home/baseq3/scripts/lgvamp.factories` (must stay valid JSON: `python3 -m json.tool home/baseq3/scripts/lgvamp.factories`), then run `docker compose restart`.
- A cvar changed over RCON (e.g. `./rcon timelimit 1`) lasts until the next restart, map changes included.
## Self-healing (`./scripts/install-cron.sh`)
| What | When | Log |
|---|---|---|
| crash restart | Docker (`restart: unless-stopped`) | `docker inspect -f '{{.RestartCount}}' ql-lg` |
| hang watchdog: 3 failed checks in a row → restart | every 5 min | `logs/watchdog.log` |
| backup of `.env`, secrets, access.txt, cfgs, plugins (14 days) | 04:40 | `backups/` |
| daily restart, skipped while a human is on | 05:00 | `logs/watchdog.log` |
`./scripts/install-cron.sh --remove` removes those cron lines. A `docker compose stop` is respected: the watchdog leaves a stopped server alone.
## Updates
- Game: `./setup.sh --update`, then `docker compose restart`. If a game update ever removes `server/run_server_x64_minqlx.sh`, plain `./setup.sh` copies it back.
- This repo: `git pull`, then `./setup.sh` (re-running it is safe: it keeps your `.env`, password and game files).
## More than one server
Clone the repo into a second folder. Before running `./setup.sh`, create `.env` there from `.env.example` with a different `QL_PORT`, `QL_RCON_PORT` and `QL_NAME` (e.g. `27961`, `28961`, `ql-lg-2`). Each folder downloads its own game files (~1.1 GB). Each server costs about 3% of a CPU core while idle.
## Logs
`docker logs --tail 50 ql-lg` (your `QL_NAME`). minqlx log: `home/minqlx.log`. Lines with the RCON password are filtered out before they reach `docker logs`, but treat logs as private anyway.
## Without minqlx
If minqlx ever breaks after a game update, change `run_server_x64_minqlx.sh` to `run_server_x64.sh` in `docker-compose.yml` and run `docker compose up -d`. Everything still works except that the warmup text ("The match will begin when more players join") comes back and there's no welcome message.
+50
View File
@@ -0,0 +1,50 @@
# 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.
+41
View File
@@ -0,0 +1,41 @@
# Playing on an LG vampiric server
## Joining
You need Quake Live (Steam). Open the console with the `~` key and type:
```
/connect <server address>:27960
```
Your server admin gives you the address. If the server is listed, it also shows up in the in-game server browser; search for "lg" or "vampiric".
## How it works
- You spawn with the **lightning gun** (and gauntlet), infinite ammo and 200 HP (100 HP on `lgvamp100` servers).
- **Vampiric:** every point of damage you deal heals you by the same amount. Health above 100 slowly decays (~1 HP per second).
- **No knockback, no items, no fall or self damage.**
- The server **stays in warmup on purpose.** There's no match to ready up for (F3 does nothing) and no scoreboard reset. Pair up with someone and duel, and switch partners whenever you like.
- **Antilag** (most servers): your hits register where you saw the enemy, up to about 240 ms ping. On `lgvamp_nolag` servers you have to lead your shots by your ping.
## Client settings people use for LG
These are client-side; type them in the console, or put them in `baseq3/autoexec.cfg` so they stick (Quake Live rewrites `qzconfig.cfg`). Ranges are from the [x76 config guide](https://x76.gg/wiki/ql-startup/); the values are preferences, not rules.
| Cvar | What it does |
|---|---|
| `cg_lightningStyle 1`–`5` | beam look (Settings → Weapons → Lightning Style) |
| `cg_lightningImpact 0` / `1` | sparks where the beam hits |
| `cg_impactSparks 0` / `1` | hit sparks on players |
| `cg_drawGun 0`–`2`, `cg_gunX/Y/Z` | gun model visibility/position (less of the screen covered) |
| `cg_fov` | field of view |
| `cg_drawInputCmds 1` | shows your key presses, useful for checking your strafing |
| `cg_enemyHeadColor` etc. | bright enemy colors, e.g. `0xFF0000FF` |
| `r_fullbright 1` | flat bright walls (easier to track the enemy) |
**No-beam LG:** some players use a "No LG Beam" workshop add-on with `cg_lightningStyle 3`. That Steam workshop item has been removed, and servers here run `sv_pure 1`, so don't count on it working.
## Practice tips
- The [x76 strafe-aim guide](https://x76.gg/wiki/strafe-aim-guide/) covers the technique LG duels are about: keep the mouse steady, track by strafing with the keyboard and correct small amounts with the mouse.
- Practice alone: ask an admin for a bot (`addbot hunter 5`). Bots jump around a lot, which makes them a fair tracking target, but they don't fight well in this mode.