Production-ready Counter-Strike 2 stats plugin for CounterStrikeSharp. Captures every game event in real time, persists it to MySQL, and aggregates it at three granularity levels: lifetime, session, and per map.
→ Live demo dashboard — a showcase site with example charts and a full catalogue of every stat the plugin can capture or derive.
- Live Demo
- Features
- How It Works
- Requirements
- Installation
- Configuration
- In-Game Commands
- Database Schema
- Updating
- Multi-Server Setup
- Webhook Milestones
- Local Development
- Contributing
neutronbzh.github.io/cs2-statplay-site — a static showcase site (source) with a full catalogue of every stat the plugin captures directly vs. every stat that can be derived from it (K/D, ADR, headshot %, multi-kills, entry kills, win rate, server presence…), plus an example dashboard built with fictional data to show what it renders like.
![]() |
![]() |
![]() |
![]() |
| Category | What is tracked |
|---|---|
| Sessions | Player connect / disconnect with playtime |
| Rounds | Round start, end, duration, winning team |
| Kills | K/D/A, headshot, hitgroup, weapon, distance, damage (health + armor) |
| Grenades | HE, flashbang, molotov, smoke detonations |
| Objectives | Bomb plants, bomb defuses, round MVP, hostage rescues/kills |
| Presence | Connected player count snapshots (configurable interval) |
| Aggregates | Lifetime stats, per-session stats, per-map stats via stored procedures |
v1.1.0 additions
!stats,!rank,!topin-game chat commands- Player display name stored and updated on each connect
winner_teamon every round- Hostage events table (
hostage_rescued,hostage_killed) - Versioned schema migrations — zero manual
ALTER TABLEon upgrades - Multi-server support — each server tracked independently
- Milestone webhook — HTTP POST when a player crosses a kill threshold
flowchart LR
A[CS2 Game Events] --> B[StatsCaptureService\nin-memory buffer]
B -->|flush every 15 s| C[MySqlStatsWriter\ntransactional batch]
C --> D[(MySQL\nraw event tables)]
D -->|stored procedures| E[(MySQL\naggregate tables)]
E --> F[Website / API / Dashboard / AI]
Key components
| File | Role |
|---|---|
CS2StatsPlugin.cs |
Plugin lifecycle, timers, flush orchestration, exponential-backoff reconnect |
StatsCaptureService.cs |
Converts CS2 game events to typed contracts, buffers them in memory |
MySqlStatsWriter.cs |
Drains the buffer into MySQL inside a single transaction per flush |
DatabaseInitializationService.cs |
Auto-creates database, tables, and applies versioned migrations on startup |
AggregationService.cs |
Calls stored procedures to refresh aggregate tables after each flush |
StatsCommandService.cs |
Powers the !stats, !rank, !top chat commands |
MilestoneWebhookService.cs |
Fires HTTP POST webhooks when players reach kill milestones |
- Counter-Strike 2 dedicated server (Linux x64)
- MetaMod:Source installed
- CounterStrikeSharp installed
- MySQL 8+ (or MariaDB 10.5+)
CREATE DATABASE cs2_stats CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'cs2stats'@'%' IDENTIFIED BY 'your-secure-password';
GRANT ALL PRIVILEGES ON cs2_stats.* TO 'cs2stats'@'%';
FLUSH PRIVILEGES;The plugin auto-creates all tables and applies migrations on first start. No manual SQL import needed.
Go to Releases and download:
CS2-STATPLAY-x.x.x-linux-x64.zip— full install (includes default config)CS2-STATPLAY-x.x.x-linux-x64-update-no-config.zip— update only (preserves your existing config)
# From your game/csgo/ directory
unzip CS2-STATPLAY-x.x.x-linux-x64.zipFinal paths after extraction:
game/csgo/addons/counterstrikesharp/plugins/CS2Stats/CS2Stats.dll
game/csgo/addons/counterstrikesharp/configs/plugins/CS2Stats/CS2Stats.json
Edit game/csgo/addons/counterstrikesharp/configs/plugins/CS2Stats/CS2Stats.json with your MySQL credentials (see Configuration).
On startup the plugin will:
- Connect to MySQL
- Create the database if it does not exist
- Create all tables
- Apply any pending schema migrations
- Start capturing events
Verify the plugin loaded with css_plugins list in the server console.
Full reference for CS2Stats.json:
{
"server": {
"name": "my-server-1"
},
"mySql": {
"host": "127.0.0.1",
"port": 3306,
"database": "cs2_stats",
"username": "cs2stats",
"password": "your-password",
"sslRequired": false
},
"modules": {
"sessionTrackingEnabled": true,
"kdaEnabled": true,
"weaponFireEnabled": true,
"grenadeStatsEnabled": true,
"objectiveStatsEnabled": true,
"presenceSnapshotsEnabled": true
},
"sync": {
"flushIntervalSeconds": 15,
"presenceSnapshotIntervalSeconds": 10,
"maxBufferedEvents": 5000
},
"webhook": {
"url": "",
"killsMilestone": 100
}
}server
| Field | Default | Description |
|---|---|---|
name |
"default" |
Identifier stored in the servers table. Use a unique name per server when running multiple instances against the same database. |
mySql
| Field | Default | Description |
|---|---|---|
host |
"127.0.0.1" |
MySQL host |
port |
3306 |
MySQL port |
database |
"cs2_stats" |
Database name (auto-created if the user has CREATE privilege) |
username |
"cs2stats" |
MySQL user |
password |
"change-me" |
MySQL password |
sslRequired |
false |
Enforce TLS for the MySQL connection |
modules — set any field to false to stop collecting that category of data
| Field | Default | Description |
|---|---|---|
sessionTrackingEnabled |
true |
Player connect / disconnect sessions |
kdaEnabled |
true |
Kill / death / assist events |
weaponFireEnabled |
true |
Weapon fire counts |
grenadeStatsEnabled |
true |
Grenade detonations |
objectiveStatsEnabled |
true |
Bomb, MVP, hostage events |
presenceSnapshotsEnabled |
true |
Periodic connected player count snapshots |
sync
| Field | Default | Description |
|---|---|---|
flushIntervalSeconds |
15 |
How often the in-memory buffer is written to MySQL |
presenceSnapshotIntervalSeconds |
10 |
How often a presence snapshot is captured |
maxBufferedEvents |
5000 |
Maximum events held in memory before new events are dropped |
webhook
| Field | Default | Description |
|---|---|---|
url |
"" |
HTTP(S) endpoint to POST to. Leave empty to disable. |
killsMilestone |
100 |
Fire a webhook every time a player's in-session kill count crosses a multiple of this value (100, 200, 300…) |
Players type these in chat (the ! prefix is handled automatically by CounterStrikeSharp).
| Command | Description |
|---|---|
!stats |
Your own stats: K/D/A, HS%, ADR, MVPs, playtime |
!stats <name> |
Stats for any currently online player (partial name, case-insensitive) |
!rank |
Your global kill ranking among all tracked players |
!top |
Top 5 players by total kills |
The plugin manages two layers of tables automatically.
Written directly by the plugin on each flush.
| Table | Description |
|---|---|
players |
One row per Steam account. Stores steam_id64 and display_name. |
servers |
One row per unique server.name. |
map_sessions |
One row per map load, linked to a server. |
player_sessions |
Connect / disconnect record per player per map session. |
rounds |
Round start / end with duration and winner_team. |
kill_events |
Full kill detail: weapon, hitgroup, damage, flags (HS / noscope / blind…). |
player_action_events |
Weapon fire, grenades, bomb plants/defuses, MVP — typed action rows. |
hostage_events |
Hostage rescues and kills with player and round context. |
presence_snapshots |
Periodic snapshots of connected player count. |
schema_migrations |
Tracks applied migrations (internal — do not modify). |
Refreshed automatically by stored procedures after each flush.
| Table | Description |
|---|---|
player_lifetime_stats |
Cumulative totals per player across all sessions. |
player_session_stats |
Totals per player per connection session. |
player_map_stats |
Totals per player per map session. |
You can also call the stored procedures directly:
-- Refresh all players
CALL sp_refresh_player_lifetime_stats(NULL);
-- Refresh a specific player (player_id = 42)
CALL sp_refresh_player_lifetime_stats(42);
-- Refresh a specific session
CALL sp_refresh_player_session_stats(7);
-- Refresh a specific player on a specific map session
CALL sp_refresh_player_map_stats(42, 3);For a complete field-by-field reference with example queries, see docs/STATS_DATA_REFERENCE.md.
Always use the -update-no-config package when upgrading an existing installation. It replaces the plugin binary without touching your CS2Stats.json.
unzip CS2-STATPLAY-x.x.x-linux-x64-update-no-config.zipSchema migrations are applied automatically on the next server start — no manual SQL needed.
Point multiple game servers at the same MySQL database and set a unique server.name in each config:
// Server 1 — competitive
{ "server": { "name": "eu-competitive-1" } }
// Server 2 — deathmatch
{ "server": { "name": "eu-deathmatch-1" } }Each server gets its own row in the servers table. All map_sessions carry a server_id foreign key, so you can filter or aggregate stats per server in SQL.
When webhook.url is set, the plugin sends an HTTP POST every time a player's kill count (tracked since server start) crosses a multiple of webhook.killsMilestone.
Example payload
{
"event": "milestone",
"server": "my-server-1",
"steamId64": "76561198000000001",
"playerName": "PlayerName",
"milestone": "kills",
"value": 100,
"timestamp": "2025-06-09T14:32:00Z"
}The request is fire-and-forget and never blocks the game thread. Network errors are logged as warnings and do not affect plugin operation. Leave url empty to disable this feature entirely.
dotnet restore CSStat.sln
dotnet build CSStat.sln
dotnet test CSStat.sln./scripts/package-release.ps1 `
-Configuration Release `
-Version 1.1.0 `
-PackageId CS2-STATPLAY `
-RuntimeIdentifier linux-x64Output:
artifacts/
CS2-STATPLAY-1.1.0-linux-x64.zip
CS2-STATPLAY-1.1.0-linux-x64-update-no-config.zip
SHA256SUMS.txt
CSStat.sln
├── src/
│ ├── CS2Stats.Contracts/ # Shared event records and config types
│ ├── CS2Stats.Plugin/ # Plugin, writer, capture, commands, webhook
│ └── CS2Stats.Tests/ # Unit tests (xUnit)
├── sql/ # Baseline schema and stored procedures
├── config/ # Example config file
├── scripts/ # Release packaging script
└── docs/ # Data reference and architecture notes
See CONTRIBUTING.md for guidelines.
Security issues: see SECURITY.md.
License: MIT.



