Add 4 tutorials (4 files)
Browse Source
4 changed files
with 181 additions and 0 deletions
A
cs16-missing-map-when-joining/en.md
+51 -0
A
fix-error-017-undefined-symbol/en.md
+57 -0
A
mapcycle-txt-setup/en.md
+62 -0
A
measure-server-fps/en.md
+11 -0
cs16-missing-map-when-joining/en.md
A
| @@ -0,0 +1,51 @@ | ||
| 1 | +--- | |
| 2 | +title: "Fix: 'Missing map' When Joining a Server" | |
| 3 | +excerpt: "You click a server, the download bar stalls or errors, and you drop out with 'Missing map'. Here is why it happens and how to actually get onto the server." | |
| 4 | +category: "client" | |
| 5 | +tags: "maps, downloads" | |
| 6 | +image: "" | |
| 7 | +read_time: "6 min read" | |
| 8 | +--- | |
| 9 | +<p>You pick a server, the loading screen shows a download bar, and then instead of spawning you land back at the menu with <code>Missing map "de_somemap"</code> or a console line like <code>Host_Error: Server didn't send the requested resource</code>. This is almost always a client-side download problem, not a broken server. CS 1.6 servers ship custom maps to you over an HTTP mirror (FastDL) or over the slow in-game UDP channel, and any link in that chain can fail. This guide walks the whole chain from the client side.</p> | |
| 10 | + | |
| 11 | +<h2>1. Confirm downloads are enabled in your client</h2> | |
| 12 | +<p>If your client refuses downloads outright, every custom-map server will drop you. Open the console (bind a key or launch with <code>-console</code>) and check:</p> | |
| 13 | +<pre>cl_allowdownload 1 | |
| 14 | +cl_allowupload 1 | |
| 15 | +cl_download_ingame 1</pre> | |
| 16 | +<p><code>cl_allowdownload 0</code> is the single most common cause of this error — some "FPS config" packs ship with it disabled. Set all three to 1, then reconnect.</p> | |
| 17 | + | |
| 18 | +<h2>2. Delete the half-downloaded or corrupt map</h2> | |
| 19 | +<p>If a previous download was interrupted, you now have a truncated <code>.bsp</code> that the engine treats as present but unusable, so it never re-downloads it. Delete it and let the server send a clean copy. Maps live in:</p> | |
| 20 | +<pre>Steam\steamapps\common\Half-Life\cstrike\maps\ | |
| 21 | +cstrike_downloads\maps\ (custom/downloaded content on some builds)</pre> | |
| 22 | +<p>Delete the offending <code>de_somemap.bsp</code> (and any <code>.res</code> next to it), then rejoin. On non-Steam builds the download folder is usually <code>cstrike_downloads\</code> — check both.</p> | |
| 23 | + | |
| 24 | +<h2>3. Get the map manually when the server has no FastDL</h2> | |
| 25 | +<p>If the server has no HTTP mirror configured, the map comes over the in-game channel, which is capped very low and frequently times out on large maps. The reliable workaround is to fetch the map yourself. If you know a trusted source for that exact map, drop the <code>.bsp</code> into <code>cstrike\maps\</code> before you connect. The server then sees you already have it and skips the download entirely.</p> | |
| 26 | +<blockquote>Only place maps you trust into your game folder. A malicious <code>.bsp</code> is rare, but overlay files (sprites, sounds) referenced by a <code>.res</code> file are a real vector — stick to well-known map sources.</blockquote> | |
| 27 | + | |
| 28 | +<h2>4. Rule out a client that is too new</h2> | |
| 29 | +<p>After the 2023 25th Anniversary update, some servers and download setups broke against the new client. If custom-content servers consistently fail for you but simple <code>de_dust2</code> servers work, try rolling the client back to the <strong>steam_legacy</strong> beta branch (right-click Counter-Strike in Steam, Properties, Betas). Legacy behaves like the pre-2023 client that most 1.6 servers were built and tested against.</p> | |
| 30 | + | |
| 31 | +<h2>5. Understand the server side (so you can tell the admin)</h2> | |
| 32 | +<p>When the download is merely slow rather than failing, the server admin has not set up FastDL. FastDL is an HTTP mirror declared on the server with <code>sv_downloadurl</code>; the client pulls maps, models and sounds over HTTP at full speed instead of the throttled UDP channel. The in-game channel is capped at a few KB/s by <code>sv_downloadurl</code>'s absence, so a 10 MB map can take minutes and frequently times out before it finishes. If you run the server yourself, see <a href="/tutorials/fix-fastdl-not-working">FastDL not working</a> and <a href="/tutorials/setup-fastdl-http-server">setting up a FastDL server</a>.</p> | |
| 33 | + | |
| 34 | +<h3>Why the map is "missing" even after downloading</h3> | |
| 35 | +<p>The engine validates each downloaded file against a checksum the server sends. If your on-disk copy differs by even one byte — a truncated download, or an older version of the same map name — the client rejects it and reports the map as missing rather than loading a mismatched file. This is why deleting the local copy (step 2) is so often the fix: it forces a fresh, checksum-matched download from the server or its mirror.</p> | |
| 36 | + | |
| 37 | +<h2>Common errors</h2> | |
| 38 | +<ul> | |
| 39 | +<li><strong><code>Missing map "mapname"</code></strong> — the download never completed. Enable <code>cl_allowdownload 1</code>, delete any partial <code>.bsp</code>, reconnect.</li> | |
| 40 | +<li><strong><code>Server didn't send the requested resource</code></strong> — the server referenced a file it does not actually have on disk or on its FastDL mirror. This is a server misconfiguration; nothing you change client-side fixes it.</li> | |
| 41 | +<li><strong>Download bar reaches 99% then drops</strong> — the file exists but is a different size than the server's checksum expects, usually a stale FastDL mirror. Delete your local copy and retry; if it persists, it is the server's problem.</li> | |
| 42 | +<li><strong>Everything downloads but you still can't join</strong> — you may be hitting <code>cl_download_ingame 0</code>. Set it to 1.</li> | |
| 43 | +</ul> | |
| 44 | + | |
| 45 | +<h2>Verification</h2> | |
| 46 | +<p>After the fixes, open the console before connecting and watch the download lines scroll:</p> | |
| 47 | +<pre>Verifying and downloading resources... | |
| 48 | +Downloading: maps/de_somemap.bsp</pre> | |
| 49 | +<p>When it finishes cleanly you go straight to the spawn screen. To confirm the map is now cached, type in console after joining:</p> | |
| 50 | +<pre>maps de_somemap*</pre> | |
| 51 | +<p>It should list the map as present. If it does, the next time you join that server there is no download at all. If a specific server still fails every single time while others work, the fault is on that server's content setup, and the fix belongs to its admin.</p> |
fix-error-017-undefined-symbol/en.md
A
| @@ -0,0 +1,57 @@ | ||
| 1 | +--- | |
| 2 | +title: "Fix: 'error 017: undefined symbol' in Pawn" | |
| 3 | +excerpt: "Why amxxpc throws error 017 when compiling an AMX Mod X plugin, and the five things that actually cause it: missing includes, wrong-version natives, typos, and scope mistakes." | |
| 4 | +category: "plugin-dev" | |
| 5 | +tags: "compiling, errors" | |
| 6 | +image: "" | |
| 7 | +read_time: "7 min read" | |
| 8 | +--- | |
| 9 | +<p><code>error 017: undefined symbol "x"</code> is the single most common failure people hit when compiling AMX Mod X plugins, and it means exactly one thing: the compiler reached an identifier it has no declaration for. Pawn is strongly typed and single-pass over declarations, so every function, native, constant and variable must be visible <em>before</em> the line that uses it. The symbol name in quotes is the whole diagnosis — read it first, then work through the causes below in order.</p> | |
| 10 | + | |
| 11 | +<h2>1. A missing <code>#include</code></h2> | |
| 12 | +<p>Natives live in include files, not in the compiler. If you call <code>get_user_flags()</code> without <code>#include <amxmodx></code>, or a Ham function without <code>#include <hamsandwich></code>, you get 017 on that exact native. Every plugin needs at least:</p> | |
| 13 | +<pre>#include <amxmodx> | |
| 14 | +#include <amxmisc></pre> | |
| 15 | +<p>Then add one line per module you touch — <code><fakemeta></code>, <code><hamsandwich></code>, <code><cstrike></code>, <code><fun></code>, <code><sqlx></code>. Map the undefined symbol to the include that declares it. If you are new to this, <a href="/tutorials/write-first-amxx-plugin">writing your first plugin</a> walks through the standard header block.</p> | |
| 16 | + | |
| 17 | +<h2>2. The native exists — but not in your AMXX version</h2> | |
| 18 | +<p>This trap catches experienced coders. <code>client_print_color()</code> was added in <strong>AMX Mod X 1.9</strong>. If your <code>include/</code> folder is from 1.8.2, the compiler has never heard of it and throws 017. The fix is not to hack the include — it is to compile against 1.9 or 1.10 headers and set your plugin's minimum to match. The reverse also happens: you copied a single <code>.inc</code> into a 1.8.2 tree without the natives behind it. Confirm which toolchain you are on before you blame the code; see <a href="/tutorials/compile-sma-to-amxx">compiling .sma to .amxx</a> for pinning the compiler version.</p> | |
| 19 | + | |
| 20 | +<h2>3. A typo or a case mismatch</h2> | |
| 21 | +<p>Pawn identifiers are case-sensitive. <code>get_user_Name</code> is a different symbol from <code>get_user_name</code>, and the compiler will not guess. Watch for:</p> | |
| 22 | +<ul> | |
| 23 | +<li>Capitalisation: <code>Float:</code> the tag versus <code>float</code>.</li> | |
| 24 | +<li>Underscores: <code>is_user_alive</code>, not <code>isuseralive</code>.</li> | |
| 25 | +<li>A constant you meant to <code>#define</code> but spelled differently at the use site.</li> | |
| 26 | +</ul> | |
| 27 | + | |
| 28 | +<h2>4. Using a symbol before it is declared</h2> | |
| 29 | +<p>Pawn reads top to bottom. A <code>#define</code> or a global used above its declaration is undefined at that point:</p> | |
| 30 | +<pre>public plugin_init() { | |
| 31 | + set_task(TASK_TIME, "tick") // 017: TASK_TIME not yet defined | |
| 32 | +} | |
| 33 | +#define TASK_TIME 1.0</pre> | |
| 34 | +<p>Move constants and globals to the top of the file. Functions are more forgiving — Pawn resolves ordinary function calls in a later pass — but constants and globals must precede their first use.</p> | |
| 35 | + | |
| 36 | +<h2>5. A public callback that was never written</h2> | |
| 37 | +<p>When you register a handler by name, the compiler must find a <code>public</code> function with that exact name:</p> | |
| 38 | +<pre>register_event("DeathMsg", "OnDeath", "a") | |
| 39 | +// ... | |
| 40 | +public OnDeath(id) { /* ... */ } // must exist, spelled identically</pre> | |
| 41 | +<p>Note that a missing callback more often shows as <code>error 017</code> only if you reference the function elsewhere; a bare string mismatch fails silently at runtime instead. Still, keep the registered string and the <code>public</code> name identical.</p> | |
| 42 | + | |
| 43 | +<h2>Common errors that look like 017 but are not</h2> | |
| 44 | +<ul> | |
| 45 | +<li><strong><code>error 021: symbol already defined</code></strong> — the opposite problem: two globals or two functions share a name. Rename one.</li> | |
| 46 | +<li><strong><code>warning 213: tag mismatch</code></strong> — usually a <code>Float:</code> passed where a cell is expected. Not fatal, but fix it; it is a real bug most of the time.</li> | |
| 47 | +<li><strong><code>error 088: number of arguments does not match definition</code></strong> — the native exists and is included, you just called it with the wrong argument count. Check the include for the real signature.</li> | |
| 48 | +<li><strong>017 on a whole cluster of symbols at once</strong> — almost always a single missing <code>#include</code> near the top, not many separate bugs. Fix the include and recompile before chasing the rest.</li> | |
| 49 | +</ul> | |
| 50 | + | |
| 51 | +<h2>Verification</h2> | |
| 52 | +<p>Recompile and read the summary line. A clean build ends with something like:</p> | |
| 53 | +<pre>Header size: 1234 bytes | |
| 54 | +Code size: 5678 bytes | |
| 55 | +... | |
| 56 | +0 Errors.</pre> | |
| 57 | +<p>Only <code>0 Errors.</code> produces a usable <code>.amxx</code>; a build with errors writes nothing. If you compile inside an editor, set it up as in <a href="/tutorials/vscode-amxx-setup">the VS Code AMXX setup</a> so the exact line and symbol jump out. Once it builds, drop the <code>.amxx</code> into <code>addons/amxmodx/plugins/</code>, add it to <code>plugins.ini</code>, and confirm it loads with <code>amxx list</code> in the server console — a plugin that compiled but shows <code>bad load</code> there is a different problem (usually a missing module) covered in <a href="/tutorials/fix-amxx-module-failed-to-load">the module load fix</a>.</p> |
mapcycle-txt-setup/en.md
A
| @@ -0,0 +1,62 @@ | ||
| 1 | +--- | |
| 2 | +title: "mapcycle.txt: How to Set Your Map Rotation" | |
| 3 | +excerpt: "How the CS 1.6 engine reads mapcycle.txt, how it picks the next map, and the per-map config hook and common traps that quietly break a rotation." | |
| 4 | +category: "administration" | |
| 5 | +tags: "mapcycle, maps" | |
| 6 | +image: "" | |
| 7 | +read_time: "7 min read" | |
| 8 | +--- | |
| 9 | +<p>Map rotation on a CS 1.6 server is driven by one plain-text file, <code>mapcycle.txt</code>, and one cvar, <code>mapcyclefile</code>. It is deliberately simple, which is exactly why it fails in ways that look mysterious: a map that is not on disk, a stray comment, or the wrong filename silently changes the rotation and you spend twenty minutes wondering why the server keeps looping <code>de_dust2</code>. This covers how the file is read, how the engine chooses the next map, and the per-map config hook most admins never wire up.</p> | |
| 10 | + | |
| 11 | +<h2>1. Where the file lives and what points at it</h2> | |
| 12 | +<p>The rotation file is <code>cstrike/mapcycle.txt</code>. The engine locates it through the <code>mapcyclefile</code> cvar, which defaults to <code>mapcycle.txt</code> (a path relative to the game directory). Keeping several rotations and switching between them is a one-line change in <code>server.cfg</code>:</p> | |
| 13 | +<pre>// server.cfg | |
| 14 | +mapcyclefile "mapcycle.txt" | |
| 15 | +// or a themed rotation you keep alongside it | |
| 16 | +// mapcyclefile "cycle_gg.txt"</pre> | |
| 17 | + | |
| 18 | +<h2>2. Write the file: one map per line</h2> | |
| 19 | +<p>Each line is a map name with <strong>no</strong> <code>.bsp</code> extension and no path. Blank lines and <code>//</code> comments are ignored:</p> | |
| 20 | +<pre>de_dust2 | |
| 21 | +de_inferno | |
| 22 | +de_nuke | |
| 23 | +cs_office | |
| 24 | +de_train | |
| 25 | +// de_aztec <- disabled for now</pre> | |
| 26 | +<p>Every listed map must exist as <code>cstrike/maps/<name>.bsp</code>. The engine does not fail loudly on a missing entry — it skips it and moves to the next valid line, so a typo just quietly drops a map from the rotation. If you host these maps for download, they must also be on your FastDL mirror; see <a href="/tutorials/fastdl-setup-guide">the FastDL setup guide</a> and <a href="/tutorials/install-custom-maps-cs16-server">installing custom maps</a>.</p> | |
| 27 | + | |
| 28 | +<h2>3. Understand how the next map is chosen</h2> | |
| 29 | +<p>When a map ends (see below for what ends it), the engine finds the <em>current</em> map in <code>mapcycle.txt</code> and advances to the line after it, wrapping to the top at the end. Two consequences trip people up:</p> | |
| 30 | +<ul> | |
| 31 | +<li>If the map you are on is <strong>not</strong> in the list at all, the engine cannot find its position and falls back to the first entry. A server that always jumps to the top of the cycle after a manual <code>changelevel</code> is doing exactly this.</li> | |
| 32 | +<li>The engine tracks position by map name, not by an index it saves to disk. On a clean restart it begins from wherever the current map sits in the file.</li> | |
| 33 | +</ul> | |
| 34 | +<p>The AMX Mod X <code>nextmap</code> plugin reads this same file to compute what <code>say nextmap</code> reports and to run end-of-map handling. If you also run a vote at map end, <code>mapchooser</code> or <a href="/tutorials/rtv-nextmap-setup">Rock The Vote</a> overrides the plain cycle — see that guide, and the <a href="/plugins/csb-mapchooser">CSB Mapchooser</a> plugin.</p> | |
| 35 | + | |
| 36 | +<h2>4. What actually ends a map</h2> | |
| 37 | +<p>The rotation only advances when the map ends, and that is governed by cvars, not by <code>mapcycle.txt</code>:</p> | |
| 38 | +<pre>mp_timelimit 30 // minutes; 0 disables the time-based change | |
| 39 | +mp_maxrounds 0 // ReGameDLL: end after N rounds (0 = off) | |
| 40 | +mp_winlimit 0 // ReGameDLL: end when a team reaches N wins</pre> | |
| 41 | +<p>With <code>mp_timelimit 0</code> and no round/win limit the map never ends on its own and the cycle appears frozen. These and the rest of the round cvars are covered in the <a href="/tutorials/mp-cvars-reference">mp_ cvars reference</a>.</p> | |
| 42 | + | |
| 43 | +<h2>5. Per-map configs with mapchangecfgfile</h2> | |
| 44 | +<p>Vanilla HLDS does not auto-execute a <code><mapname>.cfg</code> for you, but it does expose <code>mapchangecfgfile</code>, a config it runs on <em>every</em> map change. Point it at a small script that re-applies per-rotation settings, or use it to <code>exec</code> a map-specific file yourself:</p> | |
| 45 | +<pre>// server.cfg | |
| 46 | +mapchangecfgfile "onchange.cfg"</pre> | |
| 47 | +<p>Keep gameplay cvars in <code>server.cfg</code> (executed at start) and only put things that must re-apply after a map load in the change config. Per-map tuning by name is better handled by an AMXX plugin that reads the current map, but <code>mapchangecfgfile</code> is the stock hook and it is reliable.</p> | |
| 48 | + | |
| 49 | +<h2>Common errors</h2> | |
| 50 | +<ul> | |
| 51 | +<li><strong>Rotation ignores your edits</strong> — you edited the wrong file. Check what <code>mapcyclefile</code> actually resolves to in the console; if you set it to a custom name in <code>server.cfg</code>, <code>mapcycle.txt</code> is dead weight.</li> | |
| 52 | +<li><strong>A map never appears</strong> — the <code>.bsp</code> is missing from <code>cstrike/maps/</code>, or the line has a trailing extension (<code>de_nuke.bsp</code>) or a Windows carriage return from editing on the wrong OS. Save the file as plain LF text.</li> | |
| 53 | +<li><strong>Server always returns to the first map</strong> — you are running maps that are not in the cycle (a vote map, a manual <code>changelevel</code> to something unlisted). Add them, or accept that off-list maps reset the position.</li> | |
| 54 | +<li><strong>Map never changes at all</strong> — <code>mp_timelimit</code> is 0 with no round or win limit set. Nothing is telling the map to end.</li> | |
| 55 | +</ul> | |
| 56 | + | |
| 57 | +<h2>Verification</h2> | |
| 58 | +<p>From RCON or the server console, load the first map and query the plugin view:</p> | |
| 59 | +<pre>changelevel de_dust2 | |
| 60 | +// then in-game | |
| 61 | +say nextmap</pre> | |
| 62 | +<p><code>say nextmap</code> should report the line that follows your current map in the file. Let <code>mp_timelimit</code> expire once (or lower it to 1 for a quick test) and confirm the server advances to that map rather than reloading the same one. Once the cycle behaves, layer voting on top with <a href="/tutorials/rtv-nextmap-setup">the RTV and nextmap guide</a>.</p> |
measure-server-fps/en.md
A
| @@ -0,0 +1,11 @@ | ||
| 1 | +--- | |
| 2 | +title: "How to Measure Real Server FPS (stats and ReHLDS Tools)" | |
| 3 | +excerpt: "Server FPS is the tick loop rate, not the client frame rate. Here is how to read the real number from the HLDS console, over RCON, and under load." | |
| 4 | +category: "optimization" | |
| 5 | +tags: "fps, monitoring" | |
| 6 | +image: "" | |
| 7 | +read_time: "7 min read" | |
| 8 | +--- | |
| 9 | +<p>"Server FPS" has nothing to do with what you see on your screen. On a CS 1.6 dedicated server it is the rate of the engine's main loop — how many times per second the server simulates the world, processes player commands and sends updates. A server pinned at 1000 FPS feels tight and responsive; one that sags to 40 under a full house feels like every shot lands late. Before you tune anything, you need to measure the real number honestly, because the target you set with <code>sys_ticrate</code> and the number you actually get are frequently not the same.</p><h2>1. Read it with the stats command</h2><p>The console command <code>stats</code> is built into HLDS and ReHLDS. Type it in the server console (or over RCON) and it prints a single line:</p><pre>stats</pre><p>The output is roughly:</p><pre>CPU In_(KB/s) Out_(KB/s) Uptime Users FPS Players | |
| 10 | +6.30 2.45 9.81 180 4 999.85 4</pre><p>The column that matters is <strong>FPS</strong>. <code>CPU</code> is the percentage of one core the server is burning, <code>In</code>/<code>Out</code> are bandwidth, and <code>Players</code> is the live count. Run <code>stats</code> a few times a second apart — a single reading tells you almost nothing because FPS fluctuates frame to frame.</p><h2>2. Compare the target against the reality</h2><p>The server tries to run at the rate set by <code>sys_ticrate</code>. Check what you asked for:</p><pre>sys_ticrate</pre><p>If <code>sys_ticrate</code> is 1000 but <code>stats</code> reports 512, or 200, the server cannot sustain the target on this hardware or with this launch configuration. That gap — not the raw number — is the diagnostic. A machine holding a steady 500 FPS is healthier than one that claims to target 1000 and jitters between 300 and 1000.</p><h2>3. Measure over RCON from outside</h2><p>You do not need shell access to the box. From a connected client's console, authenticate and query:</p><pre>rcon_password your_rcon_password | |
| 11 | +rcon stats</pre><p>This is the honest way to check a server you rent from a host — the panel's "FPS" widget is often the configured <code>sys_ticrate</code>, not the measured value. <code>rcon stats</code> returns the same line the local console does.</p><h2>4. Measure under load, not empty</h2><p>An empty server will happily report its target FPS. The number that matters is the one with a full server mid-round, because every connected player adds work to each frame. If you cannot wait for real players, add bots (for example with <a href="/tutorials/install-yapb-bots">YaPB</a>) to load the CPU, then watch <code>stats</code> during a firefight. A server that reads 1000 empty and 250 with 24 players and bots does not have a 1000 FPS problem — it has a CPU problem.</p><h2>5. Watch CPU alongside FPS</h2><p>The <code>CPU</code> column in <code>stats</code> is the single most useful companion to the FPS number. HLDS is single-threaded, so it can only ever use one core; a reading near 100 means that core is saturated and FPS will fall no matter what <code>sys_ticrate</code> says. On Linux, confirm from the shell:</p><pre>top -p $(pgrep -f hlds_linux)</pre><p>If <code>hlds_linux</code> sits at 100% of a core while FPS is below target, the loop is CPU-bound. That points you at the engine (stock HLDS timing), the <code>-pingboost</code> mode, or a heavy plugin — see <a href="/tutorials/fix-hlds-100-cpu">HLDS at 100% CPU</a> and <a href="/tutorials/fix-low-server-fps-linux">low server FPS on Linux</a>.</p><h2>6. ReHLDS reports the same way</h2><p>ReHLDS uses the identical <code>stats</code> command, so nothing about measuring changes. What changes is that ReHLDS keeps far more accurate frame timing than stock HLDS, so a <code>sys_ticrate</code> of 1000 is much more likely to actually be held. If you are chasing a stable high number, the engine swap does more than any cvar — see <a href="/tutorials/rehlds-performance-cvars">ReHLDS performance cvars</a> and <a href="/tutorials/stable-1000-fps-linux-server">getting stable 500/1000 FPS on Linux</a>.</p><h2>Common errors and misreadings</h2><ul><li><strong>Reading FPS from a control panel instead of <code>stats</code></strong> — many panels display the configured tick target, not the measured rate. Always confirm with <code>rcon stats</code>.</li><li><strong>Measuring an empty server and declaring victory</strong> — load changes everything. Measure with players or bots.</li><li><strong>Confusing server FPS with client <code>fps_max</code></strong> — they are unrelated numbers. Boosting your own client does nothing for the server tick; see <a href="/tutorials/cs16-fps-boost-guide">the client FPS guide</a>.</li><li><strong>Expecting a Windows server to hit Linux numbers</strong> — the Windows timer caps sustained FPS far lower; 100–200 is normal there.</li><li><strong>FPS looks fine but players complain of lag</strong> — that is a rate/choke problem, not an FPS problem. Check the scoreboard for loss and choke, covered in <a href="/tutorials/fix-choke-loss-scoreboard">choke and loss on the scoreboard</a>.</li></ul><h2>Verification</h2><p>You have a trustworthy measurement when: <code>stats</code> gives you a stable FPS reading across several samples; that reading is taken with the server under realistic load; and the <code>CPU</code> column is well under 100 at your target rate. Note the empty-server FPS and the full-server FPS separately — those two numbers are what every tuning change should be judged against. With a real baseline in hand, move on to <a href="/tutorials/sv-maxrate-maxupdaterate-values">setting correct rate cvars</a> and, if the number is low, <a href="/tutorials/stable-1000-fps-linux-server">the Linux FPS guide</a>.</p> | |
| 0 | 12 |









