[ 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:

[ See Also ]