Add 2 tutorials (2 files)
Browse Source
2 changed files
with 167 additions and 0 deletions
fix-hlds-100-cpu/en.md
A
| @@ -0,0 +1,61 @@ | ||
| 1 | +--- | |
| 2 | +title: "Fix: hlds_linux Using 100% CPU" | |
| 3 | +excerpt: "When 100% CPU on hlds_linux is normal busy-waiting and when it is a real problem - pingboost modes, a runaway plugin, empty-server spin, and how to tell them apart." | |
| 4 | +category: "errors" | |
| 5 | +tags: "cpu, performance" | |
| 6 | +image: "" | |
| 7 | +read_time: "8 min read" | |
| 8 | +--- | |
| 9 | +<p><code>top</code> shows <code>hlds_linux</code> eating a whole core and you assume something is wrong. Often nothing is: the Half-Life engine deliberately busy-waits to hold a high server FPS, and a busy-wait <em>is</em> 100% of a core by definition. The real question is whether that CPU is buying you stable frames or being wasted. This guide separates "working as designed" from "actually broken" and fixes the broken cases.</p> | |
| 10 | + | |
| 11 | +<h2>1. Decide whether it is even a problem</h2> | |
| 12 | +<p>Check FPS and CPU together on the server console and host:</p> | |
| 13 | +<pre>stat</pre> | |
| 14 | +<pre>top -H -p $(pgrep -f hlds_linux)</pre> | |
| 15 | +<p>If <code>stat</code> shows a high, stable FPS (say 1000) and the box is otherwise responsive, the 100% is your busy-wait doing its job — that is the cost of high server FPS on this engine, and "fixing" it means accepting lower FPS. If instead FPS is low <em>and</em> CPU is pinned, or CPU is pinned with an <strong>empty</strong> server, you have a genuine problem.</p> | |
| 16 | + | |
| 17 | +<h2>2. Empty-server spin</h2> | |
| 18 | +<p>A server that pins a core with zero players is the clearest red flag. Causes:</p> | |
| 19 | +<ul> | |
| 20 | +<li>An aggressive <code>-pingboost</code> mode busy-waiting hard even when idle.</li> | |
| 21 | +<li>A plugin running a tight <code>set_task</code> loop or a per-frame hook doing real work every frame.</li> | |
| 22 | +<li><code>sys_ticrate</code> set absurdly high (e.g. 4000+) so the engine spins to hit an impossible frame count.</li> | |
| 23 | +</ul> | |
| 24 | +<p>Lower the ticrate to something sane and re-measure:</p> | |
| 25 | +<pre>sys_ticrate 1000</pre> | |
| 26 | + | |
| 27 | +<h2>3. Tune or drop -pingboost</h2> | |
| 28 | +<p>The <code>-pingboost</code> launch modes trade CPU for FPS. Mode 0 (or omitting it) uses the gentlest sleep; higher modes busy-wait. If idle CPU is the complaint, step down:</p> | |
| 29 | +<pre>./hlds_run -game cstrike -pingboost 0 -sys_ticrate 1000 \ | |
| 30 | + -ip 0.0.0.0 -port 27015 +maxplayers 20 +map de_dust2</pre> | |
| 31 | +<p>Measure FPS after each change. You are looking for the lowest CPU that still holds the FPS you need — see <a href="/tutorials/fix-low-server-fps-linux">the server FPS guide</a> for the FPS side of this same trade-off.</p> | |
| 32 | + | |
| 33 | +<h2>4. Hunt a runaway plugin</h2> | |
| 34 | +<p>If dropping pingboost does not calm an idle server, a plugin is doing per-frame work. Pause AMXX and watch CPU:</p> | |
| 35 | +<pre>amxx pause GamePlugins</pre> | |
| 36 | +<p>If CPU falls immediately, a plugin is the cause. Bisect with <a href="/tutorials/find-crashing-plugin">the plugin-isolation method</a> — a stats plugin polling a database every frame, a HUD updater with no rate limit, or a broken <code>set_task</code> repeating far too often are the usual offenders.</p> | |
| 37 | + | |
| 38 | +<h2>5. Move to ReHLDS and pin a core</h2> | |
| 39 | +<p><a href="/tutorials/install-rehlds-on-linux">ReHLDS</a> reaches a given FPS with meaningfully less CPU than stock HLDS pingboost tricks, so the same 1000 FPS costs you less core. On a multi-core host, also pin each server to its own core so a spike on one instance does not starve another:</p> | |
| 40 | +<pre>taskset -c 1 ./hlds_run -game cstrike -sys_ticrate 1000 \ | |
| 41 | + +maxplayers 20 +map de_dust2</pre> | |
| 42 | +<p>On a shared VPS you may simply be fighting noisy neighbours; there the only real fixes are ReHLDS efficiency and a better host.</p> | |
| 43 | + | |
| 44 | +<h2>6. Read steal time before you blame the engine</h2> | |
| 45 | +<p>Before you spend an evening tuning, check whether the CPU is even yours to spend. On a VPS, the <code>%st</code> (steal) column in <code>top</code> shows time the hypervisor took from your vCPU:</p> | |
| 46 | +<pre>top # watch the %st column while the server runs</pre> | |
| 47 | +<p>High steal means the number you see for <code>hlds_linux</code> is inflated by contention you do not control — the process looks pinned partly because it keeps getting scheduled off and has to catch up. On a dedicated box steal is essentially zero and 100% genuinely reflects the busy-wait. This one column decides whether the fix is "tune pingboost/ticrate" (your box, your problem) or "move hosts" (someone else's box, not fixable in a cvar).</p> | |
| 48 | + | |
| 49 | +<h2>Common errors</h2> | |
| 50 | +<ul> | |
| 51 | +<li><strong>100% CPU but high, stable FPS and a responsive box</strong> — working as designed. Busy-wait equals a full core. Lower <code>sys_ticrate</code>/pingboost only if you want to trade FPS for CPU.</li> | |
| 52 | +<li><strong>100% CPU on an empty server</strong> — pingboost too aggressive, ticrate too high, or a per-frame plugin. Start with ticrate and pingboost.</li> | |
| 53 | +<li><strong>CPU climbs over time</strong> — a plugin leak or a task scheduling more tasks than it clears. Bisect plugins.</li> | |
| 54 | +<li><strong>Multiple servers each fighting for CPU</strong> — pin them to separate cores with <code>taskset</code> and cap ticrate per instance.</li> | |
| 55 | +</ul> | |
| 56 | + | |
| 57 | +<h2>Verification</h2> | |
| 58 | +<p>After changes, watch an idle server and a loaded one:</p> | |
| 59 | +<pre>stat | |
| 60 | +top -p $(pgrep -f hlds_linux)</pre> | |
| 61 | +<p>Healthy looks like: near-zero real work on an empty server if you dropped pingboost, or a full core that <em>buys</em> a stable high FPS if you kept it — and under a full match, CPU that scales with players and never starves the host. If pausing all plugins drops CPU to near idle, you found a plugin; if it does not, the cost is engine/timer and ReHLDS plus a lower ticrate is your lever.</p> |
write-first-amxx-plugin/en.md
A
| @@ -0,0 +1,106 @@ | ||
| 1 | +--- | |
| 2 | +title: "Write Your First AMX Mod X Plugin (Hello World)" | |
| 3 | +excerpt: "A complete, working first plugin: register a chat command that reports a player's health, compiled and installed step by step. Real Pawn, not pseudocode." | |
| 4 | +category: "plugin-dev" | |
| 5 | +tags: "amxmodx, beginner" | |
| 6 | +image: "" | |
| 7 | +read_time: "8 min read" | |
| 8 | +--- | |
| 9 | +<p>The fastest way to understand AMX Mod X is to write a plugin that does something real, compile it, and watch it run on your server. This one registers a chat command — typing <code>/hp</code> reports your health and armor — and greets players when they connect. It is short, it compiles, and it touches the three things every plugin uses: <code>plugin_init</code>, a command callback, and a client event. If the Pawn syntax here is unfamiliar, read <a href="/tutorials/pawn-basics-for-amxx">Pawn language basics</a> first.</p> | |
| 10 | + | |
| 11 | +<h2>1. Create the .sma source</h2> | |
| 12 | +<p>Make a file <code>myfirst.sma</code>. The <code>#include <amxmodx></code> line pulls in the core API; <code>#include <cstrike></code> gives you CS-specific natives like armor:</p> | |
| 13 | +<pre>#include <amxmodx> | |
| 14 | +#include <cstrike> | |
| 15 | + | |
| 16 | +#define PLUGIN "My First Plugin" | |
| 17 | +#define VERSION "1.0.0" | |
| 18 | +#define AUTHOR "Your Name" | |
| 19 | + | |
| 20 | +public plugin_init() { | |
| 21 | + register_plugin(PLUGIN, VERSION, AUTHOR) | |
| 22 | + register_clcmd("say /hp", "cmd_hp") | |
| 23 | + register_clcmd("say_team /hp", "cmd_hp") | |
| 24 | +} | |
| 25 | + | |
| 26 | +public cmd_hp(id) { | |
| 27 | + if (!is_user_alive(id)) { | |
| 28 | + client_print(id, print_chat, "[MyPlugin] You are dead.") | |
| 29 | + return PLUGIN_HANDLED | |
| 30 | + } | |
| 31 | + | |
| 32 | + new health = get_user_health(id) | |
| 33 | + new armor = cs_get_user_armor(id) | |
| 34 | + | |
| 35 | + new name[32] | |
| 36 | + get_user_name(id, name, charsmax(name)) | |
| 37 | + | |
| 38 | + client_print(id, print_chat, "[MyPlugin] %s - HP: %d, Armor: %d", name, health, armor) | |
| 39 | + return PLUGIN_HANDLED | |
| 40 | +} | |
| 41 | + | |
| 42 | +public client_putinserver(id) { | |
| 43 | + new name[32] | |
| 44 | + get_user_name(id, name, charsmax(name)) | |
| 45 | + client_print(0, print_chat, "[MyPlugin] Welcome, %s!", name) | |
| 46 | +}</pre> | |
| 47 | + | |
| 48 | +<h2>2. Understand what each part does</h2> | |
| 49 | +<ul> | |
| 50 | +<li><strong><code>plugin_init()</code></strong> runs once when the plugin loads. <code>register_plugin</code> is mandatory — it names the plugin in <code>amxx list</code>.</li> | |
| 51 | +<li><strong><code>register_clcmd("say /hp", "cmd_hp")</code></strong> ties the chat text <code>/hp</code> to your <code>cmd_hp</code> function. Registering both <code>say</code> and <code>say_team</code> means it works whether typed in all-chat or team-chat.</li> | |
| 52 | +<li><strong><code>cmd_hp(id)</code></strong> receives the player's id (1–32). Returning <code>PLUGIN_HANDLED</code> stops the <code>/hp</code> text from also appearing as a public chat message.</li> | |
| 53 | +<li><strong><code>client_putinserver(id)</code></strong> is a forward AMXX calls when a player finishes connecting. <code>client_print(0, ...)</code> with id <code>0</code> prints to everyone.</li> | |
| 54 | +</ul> | |
| 55 | + | |
| 56 | +<h2>3. Compile it</h2> | |
| 57 | +<p>Compile <code>myfirst.sma</code> into <code>myfirst.amxx</code>. The full walkthrough is in <a href="/tutorials/compile-sma-to-amxx">compiling a .sma into .amxx</a>; the short version on the server box is:</p> | |
| 58 | +<pre>cd /home/steam/hlds/cstrike/addons/amxmodx/scripting | |
| 59 | +./amxxpc myfirst.sma</pre> | |
| 60 | +<p>A clean compile ends with <code>Done.</code> and produces <code>compiled/myfirst.amxx</code>. Warnings about unused symbols are fine; <em>errors</em> stop the build and must be fixed.</p> | |
| 61 | + | |
| 62 | +<h2>4. Install it</h2> | |
| 63 | +<p>Copy the compiled plugin into the plugins folder and register it:</p> | |
| 64 | +<pre>cp compiled/myfirst.amxx ../plugins/myfirst.amxx</pre> | |
| 65 | +<p>Add a line to <code>addons/amxmodx/configs/plugins.ini</code>:</p> | |
| 66 | +<pre>myfirst.amxx</pre> | |
| 67 | +<p>Then reload plugins without restarting the whole server — from RCON or the server console:</p> | |
| 68 | +<pre>amxx reload</pre> | |
| 69 | +<p>Or just change the map. <code>amxx reload</code> re-reads <code>plugins.ini</code>.</p> | |
| 70 | + | |
| 71 | +<h2>5. Test in game</h2> | |
| 72 | +<p>Join the server and type <code>/hp</code> in chat. You should see your name, HP, and armor printed back to you, and no <code>/hp</code> text leaking to public chat. Connect a second client and the whole server should see the welcome line.</p> | |
| 73 | + | |
| 74 | +<h2>6. Add a cvar to toggle it</h2> | |
| 75 | +<p>Hard-coded behaviour is fine for a first plugin, but a real one is configurable. Add a cvar so admins can switch the welcome message off without recompiling. Register it in <code>plugin_init</code> and check it before printing:</p> | |
| 76 | +<pre>new g_pWelcome | |
| 77 | + | |
| 78 | +public plugin_init() { | |
| 79 | + register_plugin(PLUGIN, VERSION, AUTHOR) | |
| 80 | + register_clcmd("say /hp", "cmd_hp") | |
| 81 | + register_clcmd("say_team /hp", "cmd_hp") | |
| 82 | + g_pWelcome = register_cvar("myplugin_welcome", "1") | |
| 83 | +} | |
| 84 | + | |
| 85 | +public client_putinserver(id) { | |
| 86 | + if (!get_pcvar_num(g_pWelcome)) | |
| 87 | + return | |
| 88 | + new name[32] | |
| 89 | + get_user_name(id, name, charsmax(name)) | |
| 90 | + client_print(0, print_chat, "[MyPlugin] Welcome, %s!", name) | |
| 91 | +}</pre> | |
| 92 | +<p>Now admins can put <code>myplugin_welcome 0</code> in <code>server.cfg</code> (or better, an <code>amxx.cfg</code>) to silence the greeting. Recompile, reload, and the cvar exists immediately — verify with <code>myplugin_welcome</code> in the console.</p> | |
| 93 | + | |
| 94 | +<h2>Common errors</h2> | |
| 95 | +<ul> | |
| 96 | +<li><strong>Plugin shows as <code>bad load</code> in <code>amxx list</code></strong> — the <code>.amxx</code> was built for a newer AMXX than the server runs, or the file is corrupt. Recompile with the matching compiler; see <a href="/tutorials/install-amx-mod-x-1-9">AMX Mod X 1.9</a>.</li> | |
| 97 | +<li><strong><code>/hp</code> also appears in chat</strong> — you returned <code>PLUGIN_CONTINUE</code> (or nothing) instead of <code>PLUGIN_HANDLED</code>.</li> | |
| 98 | +<li><strong><code>cs_get_user_armor</code> undefined</strong> — you forgot <code>#include <cstrike></code>.</li> | |
| 99 | +<li><strong>Nothing happens on <code>/hp</code></strong> — the plugin is not in <code>plugins.ini</code>, or you did not <code>amxx reload</code> / change map.</li> | |
| 100 | +<li><strong>Garbage or truncated name</strong> — the <code>name</code> buffer is too small or you passed the wrong length; use <code>charsmax(name)</code>.</li> | |
| 101 | +</ul> | |
| 102 | + | |
| 103 | +<h2>Verification</h2> | |
| 104 | +<p>In the server console:</p> | |
| 105 | +<pre>amxx list</pre> | |
| 106 | +<p>Your plugin must appear as <code>running</code> (not <code>bad load</code> or <code>debug</code>). Once it does and <code>/hp</code> works in game, you have the full loop: edit, compile, install, reload. From here, add a cvar to toggle the feature and explore the <code>cstrike</code> and <code>fun</code> natives — and when you want a proper editor, set up <a href="/tutorials/vscode-amxx-setup">VS Code for AMX Mod X</a>.</p> | |
| 0 | 107 |









