[ l4d2-server-pack ]
Four small SourceMod plugins that I wrote for the Tropical Roots Maui L4D2 server. Together they handle welcome chat, per-player kill stats, admin map-jump shortcuts, and automatic campaign rotation after every finale — the four pieces of "server chrome" that turn a default L4D2 dedicated server into a 24/7 community server. Each plugin is standalone (you can drop one or four), all share the same GPL-3.0-or-later license, and none of them require a config file. Just compile, copy, load.
Live at l4d2.magikh0e.pl. For the full persistent-stats stack (Jackz's plugin, MariaDB, web dashboard, leaderboard — more than this pack's in-memory kill-counter), see the L4D2 Stats Install Guide.
[ Overview ]
Plugin Purpose Admin-gated? ─────────────────────── ───────────────────────────────── ──────────── l4d2-welcome-message staggered greetings on join no l4d2-common-kill-counter per-player kill leaderboard no l4d2-campaign-shortcuts admin map-jump (!dc, !sf, ...) yes (a flag) l4d2-finale-next-campaign auto-rotate to next campaign no
They're independent — no shared state, no required load order — but were written as a set and have a few intentional couplings worth knowing:
- welcome-message advertises commands provided by kill-counter (!kills) and your stock RTV/vote plugins. Edit the welcome text if you don't have a matching setup. - finale-next-campaign uses a 10-second delay before changelevel so kill-counter gets its end-of-map announcement window. If you swap in a different rotation plugin, keep the delay. - campaign-shortcuts can manually override the rotation mid-cycle. The finale-next-campaign index isn't disturbed by the manual jump — it just picks up where it left off on the next finale.
[ l4d2-welcome-message ]
Greets joining players with a staggered series of chat messages instead of one all-at-once info dump that scrolls past in two seconds. By the time the player has finished loading in and looked at chat, the welcome is still there and the next block is on the way.
Schedule
Per-player, starting from the join moment (OnClientPostAdminCheck): t = 3s welcome banner — aloha + server name t = 8s stats site link — https://l4d2.magikh0e.pl t = 15s command cheat-sheet — !kills, !rate, !rtv, !nominate, !votekick t = 25s house rules + closing line
Output (chat preview)
[Server] ====================================== [Server] Aloha alice! Welcome to Tropical Roots Maui - L4D2 Servah [Server] ====================================== [Server] Stats are tracked across sessions. View at: https://l4d2.magikh0e.pl [Commands] !kills - common kills | !rate - rate this map [Commands] !rtv - vote new map | !nominate - suggest one | !votekick [Rules] Be cool. No intentional griefing or team kills. [Server] End-of-map stats are auto-announced. Have fun out there!
Notes
- Bots (IsFakeClient) are skipped — only humans get the welcome. - Each timer carries the client's UserID rather than slot index, so if the player disconnects mid-stagger the callback safely no-ops via GetClientOfUserId() returning 0. - No config file. The four text blocks are inline string literals. Edit them, recompile, reload.
[ l4d2-common-kill-counter ]
Counts how many common infected each player kills, tracks headshots separately, and announces a per-player leaderboard with an MVP callout at the end of every map. Good for adding casual competition without the heavyweight ranking-mod overhead.
How it counts
Listens for the infected_death event and increments a per-client counter, filtered to: - Killer must be in-game (IsClientInGame) - Killer must not be a bot (!IsFakeClient) - Killer must be Survivor team (GetClientTeam == 2) Headshots are tracked separately via the event's headshot boolean. Per-map kill counts work by snapshotting each player's cumulative total at round_start and subtracting at announcement time — so the leaderboard reflects the chapter you just played, not the whole campaign.
Commands
Anyone can run these — not admin-gated. !kills show the current map's leaderboard to you !killcount alias for !kills Use sm_kills / sm_killcount from RCON or server console.
Auto-announce triggers
Stats fire automatically at every natural map boundary: - finale_win 3-second delay so the victory animation plays first - map_transition chapter rollover - mission_lost everyone wiped - round_end reasons 5 (chapter complete) and 8 (mission complete)
Output (chat preview)
[Stats] ===== Map Kill Counts ===== [Stats] alice: 142 zombies (27 headshots, 19.0%) [Stats] bob: 98 zombies (12 headshots, 12.2%) [Stats] charlie: 76 zombies (5 headshots, 6.6%) [Stats] dave: 34 zombies (2 headshots, 5.9%) [Stats] MVP: alice with 142 kills! [Stats] Total team kills: 350
Notes
- Stats are in-memory only. They reset when the plugin reloads or the
server restarts. For persistent stats, swap the int arrays for
SQLite or a KeyValues file (~30 lines of additions). For a fully
featured persistent-stats stack with a web UI (campaign tracking,
friendly-fire, skill plays, leaderboard at l4d2.magikh0e.pl),
see the L4D2 Stats Install Guide.
- The MVP announcement only fires if at least one human Survivor
logged a kill — silent on empty maps or zero-kill chapters.
- Only Survivor team kills count, so the Infected team's Tank/SI
damage doesn't pad numbers in Versus.
[ l4d2-campaign-shortcuts ]
Admin-only chat shortcuts to jump straight to any campaign (L4D1 + L4D2)
without having to remember c4m1_milltown_a to load Hard Rain.
Each shortcut requires the standard ADMFLAG_CHANGEMAP ("a")
admin flag.
Behavior
On any shortcut trigger:
1. Announces the switch in chat ("X is switching to Y...")
2. Waits two seconds so everyone sees what just happened
3. Issues the underlying changelevel <mapname> server command
Commands
Use either ! (chat trigger) or sm_ (RCON / server console) prefix. !campaigns list every shortcut in chat !dead Dead Center # c1m1_hotel !dc Dark Carnival # c2m1_highway !sf Swamp Fever # c3m1_plankcountry !hr Hard Rain # c4m1_milltown_a !parish The Parish # c5m1_waterfront !passing The Passing # c6m1_riverbank !sacrifice The Sacrifice # c7m1_docks !nm No Mercy # c8m1_apartment !cc Crash Course # c9m1_alleys !toll Death Toll # c10m1_caves !da Dead Air # c11m1_greenhouse !bh Blood Harvest # c12m1_hilltop !cs Cold Stream # c13m1_alpinecreek
Permissions
Uses SourceMod's built-in ADMFLAG_CHANGEMAP (the "a"
flag). Grant a player access via
addons/sourcemod/configs/admins_simple.ini:
// One line per admin — STEAM_0:X:XXXXXXXX "a"
"STEAM_0:0:12345678" "a"
Reload admins with sm_reloadadmins in console after edits.
[ l4d2-finale-next-campaign ]
On every successful finale, waits 10 seconds and then auto-rotates the server to the next campaign in a fixed order. Set-and-forget map rotation for 24/7 servers — no votes, no RTV waits.
Rotation order
13 campaigns in fixed sequence. L4D2 campaigns first, then L4D1, then wraps:
1. Dark Carnival # c2m1_highway 2. Swamp Fever # c3m1_plankcountry 3. Hard Rain # c4m1_milltown_a 4. The Parish # c5m1_waterfront 5. The Passing # c6m1_riverbank 6. The Sacrifice # c7m1_docks 7. No Mercy # c8m1_apartment 8. Crash Course # c9m1_alleys 9. Death Toll # c10m1_caves 10. Dead Air # c11m1_greenhouse 11. Blood Harvest # c12m1_hilltop 12. Cold Stream # c13m1_alpinecreek 13. Dead Center # c1m1_hotel ← then wraps to #1
Why the 10-second delay
- Players read the final score + chat reactions
- End-of-map plugins (kill-counter, share-rate, etc.) get their
announcement window before the map yanks out from under them
- Anyone separated from the rescue still sees the win cinematic
Mission-lost behavior
Wipes (mission_lost event) do NOT advance the rotation. The plugin hooks the event but the callback is a deliberate no-op, leaving L4D2's default chapter-restart behavior intact. Teams shouldn't lose their progress on the current campaign because the Tank ate everyone on the finale. To make wipes punish-advance instead, mirror Event_FinaleWin inside Event_MissionLost — ~3 lines.
State + persistence
g_CampaignIndex is in-memory. Resets to 0 (= Dark Carnival) on plugin reload / server restart / .smx replacement. Fine for most casual deploys. For sticky rotation across restarts, swap the int for a KeyValues file: // on OnPluginStart() Handle hKv = CreateKeyValues("FinaleRotation"); FileToKeyValues(hKv, "data/finale_rotation.kv"); g_CampaignIndex = KvGetNum(hKv, "index", 0); CloseHandle(hKv); // on rotation Handle hKv2 = CreateKeyValues("FinaleRotation"); KvSetNum(hKv2, "index", g_CampaignIndex); KeyValuesToFile(hKv2, "data/finale_rotation.kv"); CloseHandle(hKv2);
[ Install ]
All four plugins follow the same install pattern. Compile each .sp into
an .smx, drop the .smx into your server's plugins directory, load.
No config files; no CVars; no admin gating except for campaign-shortcuts.
# compile all four (run from where the .sp files live) spcomp l4d2-welcome-message.sp spcomp l4d2-common-kill-counter.sp spcomp l4d2-campaign-shortcuts.sp spcomp l4d2-finale-next-campaign.sp # drop the .smx files into the plugins dir cp l4d2-*.smx /path/to/csgo/addons/sourcemod/plugins/ # load without restarting the server sm plugins load l4d2-welcome-message sm plugins load l4d2-common-kill-counter sm plugins load l4d2-campaign-shortcuts sm plugins load l4d2-finale-next-campaign
Verify with sm plugins list — all four should appear
in the loaded list. From an admin account, type !campaigns
in chat; if you see the shortcut list, campaign-shortcuts is
live. Reconnect to see the staggered welcome from
welcome-message. The other two prove themselves naturally
at the next map boundary.
[ Config knobs ]
None of these plugins ship a config file. The intent is that anything worth tuning is a 1-line source edit + recompile. The values you'll likely want to change for your own deploy:
welcome-message.sp Timer_Welcome1 server name in the "Welcome to ___" line Timer_Welcome2 stats site URL (delete the whole block if you don't run one) Timer_Welcome3 command list (match it to your actual loaded plugins) Timer_Welcome4 rules wording OnClientPostAdminCheck the 3/8/15/25 second delays finale-next-campaign.sp g_NextCampaign[] reorder, add, or remove campaigns Timer_NextCampaign the 10-second post-finale delay campaign-shortcuts.sp OnPluginStart trim shortcuts you don't want exposed (e.g., remove DLC campaigns the server doesn't run) common-kill-counter.sp AnnounceStats chat formatting + color codes
[ Source ]
All four .sp files live in this directory, licensed under GPL-3.0-or-later:
