Files
ql-lg-server/docs/HOW-IT-WORKS.md
T

6.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=''.

The training bot (lgbot): aim from the character file, movement from a plugin

Quake 3 bots take their personality from a character file. home/baseq3/botfiles/bots/lgbot_c.c (registered by scripts/lgbot.bot) sets aim accuracy, aim skill, reaction time and view speed per skill level, sets jumping, crouching and rocket jumping to 0, and weights the LG above everything else. QL reads skill blocks 1, 4 and 5 and interpolates 2 and 3 between 1 and 4, which is why the file has only three. The server loads these loose files from fs_homepath alongside the ones in pak00.pk3, so no pk3 is needed (bots run on the server only, so clients don't need the files).

The stock bot AI moves like a bot: it runs at you, hops around and goes for items. So the movement is overridden. The lgbot minqlx plugin sets the bot's horizontal velocity on every server frame: sideways to its target in the current strafe direction, plus an in/out part that holds the wanted distance (capped at 70% of the speed so it still strafes while closing in). The strafe direction comes from a small pattern generator: hold one side for a random time between the min and max, then switch. Sometimes it fakes instead: a 60–120 ms tap the other way, then a new hold on the same side. Strafing sideways to someone moves you around them, and fakes keep you going the same way, so the bot would slowly circle you. The pattern therefore tracks its net sideways drift and turns back once it is lane units off centre. Ground friction removes about a quarter of the set speed before the move happens, so on the ground it sets 1.3× (measured: the bot then really moves at the set speed). An upward speed of 200–320 (a jump; QL jumps at ~275) is cancelled. Falling, slopes and jump pads are left alone. The AI still aims and shoots, so aim and movement don't interfere. Measured on a test server, the plugin costs no extra CPU you could measure.

minqlx gives bots Steam IDs from 90071996842377216 up. The plugin uses that to pick human targets only.

minqlx is started with only our plugins (qlx_plugins "nowarmuptext,welcome,lgbot"). 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.