English | Русский
Connection history and player analytics for CS2. Every session is written to MySQL with map, geo, match stats and connection quality — ready for a web panel.
Three implementations, three platforms, one database schema. The plugin exists separately for CounterStrikeSharp, SwiftlyS2 and native Metamod:Source. They share no code — they share the schema, and all three write the same tables. You can run different plugins on different servers and build a single report.
| Platform | Requirements | Archive | Installs into |
|---|---|---|---|
| CounterStrikeSharp | CSSharp ≥ 1.0.369 (hence Metamod:Source) | ConnectHistory_cssharp_<version>.zip |
addons/counterstrikesharp/plugins/ConnectHistory/ |
| SwiftlyS2 | SwiftlyS2 ≥ 1.4.9, Metamod not needed | ConnectHistory_swiftly_<version>.zip |
addons/swiftlys2/plugins/ConnectHistory/ |
| Metamod:Source | Metamod:Source ≥ 2.0.0-git1460, nothing else | ConnectHistory_metamod_{linux,windows}_<version>.zip |
addons/ConnectHistory/ |
The native target collects everything the C# targets do, with no other plugin
required. Match results are read from the player controller, and reaching it needs
exactly one game-version-dependent number — the offset of the entity system
inside IGameResourceService. It lives in gamedata.json next to the other configs,
is never overwritten by the plugin, and is edited without a rebuild when a CS2 update
moves it (take the new value from CS2Fixes gamedata, key GameEntitySystem). Nothing
else in the plugin depends on the game version. Details in
Differences between targets.
- Sessions opened on join, closed on leave. A crashed server leaves a visible record instead of nothing.
- "Who is online" is a plain
SELECT— no RCON, no A2S queries. - Full session data: SteamID64, nickname, map, disconnect reason, country/city, IP (or a salted hash), kills/deaths/assists/damage/MVP/score, rounds, team, average/min/max ping.
- Aggregates — total playtime, session count, first/last seen, nickname history.
- Online snapshots for attendance charts.
- Survives database outages — records are spooled to disk and re-sent automatically.
- Never blocks the game thread — a single background writer, one bounded connection pool.
- JSON config with a JSON Schema — your editor autocompletes fields and highlights typos.
Requires MySQL 5.7 / 8.0+ (or MariaDB 10.4+). Everything else depends on the platform — see the table above.
- Download the archive for your platform from the latest release and extract it into the root of your game server. The directory layout is already inside, together with the GeoLite2 databases.
CounterStrikeSharp
addons/counterstrikesharp/plugins/ConnectHistory/
├── ConnectHistory.dll
├── MySqlConnector.dll
├── MaxMind.GeoIP2.dll
├── MaxMind.Db.dll
├── GeoLite2-Country.mmdb
└── GeoLite2-City.mmdb
SwiftlyS2
addons/swiftlys2/plugins/ConnectHistory/
├── ConnectHistory.dll
├── MySqlConnector.dll
├── MaxMind.GeoIP2.dll
├── MaxMind.Db.dll
├── GeoLite2-Country.mmdb
└── GeoLite2-City.mmdb
Metamod:Source
One archive per OS — take ..._linux_... or ..._windows_....
addons/metamod/ConnectHistory.vdf
addons/configs/ConnectHistory/
├── Settings.json
├── Messages.json
├── Settings.schema.json
├── Messages.schema.json
└── README.txt
addons/ConnectHistory/
├── bin/linuxsteamrt64/ConnectHistory.so (or bin/win64/ConnectHistory.dll)
├── GeoLite2-Country.mmdb
└── GeoLite2-City.mmdb
Configs live in the shared addons/configs/ tree, next to every other native
CS2 plugin's — so there is no separate rule to remember for this one. The
archive already contains them, so the plugin works even where the server
directory is read-only.
The plugin looks in csgo/addons/configs/ConnectHistory/ regardless of where
the binary itself sits, and says in the console which directory it read. Configs
left at the pre-3.0.2 path addons/ConnectHistory/configs/ keep working and are
preferred when present, so an upgrade never silently drops settings — the
console says to move them.
When updating, do not unpack configs/ over an existing install — it would
overwrite your Settings.json along with the database password.
- Start the server — the plugin creates any missing config files (parent directories included) and the database tables. If it cannot write them it says so in the console, naming the exact path, instead of running silently on defaults.
- Fill in the
Databasesection ofSettings.jsonand reload the configuration (css_ch_reload,sw_ch_reloadorch_reload, depending on the platform).
Settings.json holds the database password, so the plugin keeps it at mode 600.
The archives carry only the template with an empty password — a release with a
filled-in one fails the build.
| Platform | Config directory |
|---|---|
| CounterStrikeSharp | csgo/addons/counterstrikesharp/configs/plugins/ConnectHistory/ |
| SwiftlyS2 | csgo/addons/swiftlys2/configs/plugins/ConnectHistory/ |
| Metamod:Source | csgo/addons/configs/ConnectHistory/ (fixed — not next to the binary) |
The file format is identical: Settings.json and Messages.json move between
platforms unchanged.
| File | Purpose |
|---|---|
Settings.json |
database, what to collect, storage, commands |
Messages.json |
texts for player commands, per language |
*.schema.json |
JSON Schema, regenerated on every load |
README.txt |
short reference, regenerated on every load |
gamedata.json |
native target only: the one game-version-dependent offset; never overwritten |
Key options:
| Action | CounterStrikeSharp | SwiftlyS2 | Metamod |
|---|---|---|---|
| connectivity, queue, last error | css_ch_status (@css/root) |
sw_ch_status |
ch_status (server console) |
| reload configuration | css_ch_reload (@css/root) |
sw_ch_reload |
ch_reload (server console) |
| own playtime | css_playtime (player) |
sw_playtime (player) |
!playtime / ch_playtime (player), ch_playtime <steamid64> (console) |
| recent sessions | css_lastseen (player) |
sw_lastseen (player) |
!lastseen / ch_lastseen (player), ch_lastseen <steamid64> (console) |
In the native target the player commands answer in chat, like everywhere else — both
!playtime and /playtime work, and only your own commands are swallowed, so other
chat plugins keep seeing everything else. From the server console the same commands
take a SteamID64 and answer in the console, which is how you ask about a player who
is not on the server right now.
Six tables, all created by the plugin: ch_sessions, ch_players, ch_nicknames,
ch_servers, ch_online_snapshots, ch_schema_version.
Full schema and a query cookbook: docs/DATABASE.md.
A ready-made section for the Flute CMS panel: ConnectHistory-Flute — online list, statistics, filters and charts on top of these tables. Installation and wiring are documented in that repository.
Give the plugin its own MySQL user — it never needs DROP or DELETE:
GRANT SELECT, INSERT, UPDATE, CREATE, INDEX, ALTER ON connect_history.* TO 'ch_plugin'@'%';Each target builds on its own.
export PATH="$HOME/.dotnet:$PATH"
cd cssharp && ./build.sh # tests + archive in bin/Release/net10.0/
cd swiftly && ./build.sh # sameMySQL integration tests are skipped unless CH_TEST_MYSQL is set:
docker run --rm -d -p 3399:3306 -e MYSQL_ROOT_PASSWORD=test -e MYSQL_DATABASE=ch mysql:8
export CH_TEST_MYSQL="server=127.0.0.1;port=3399;user=root;password=test;database=ch"
dotnet test cssharp/ConnectHistory.slnThe native target is split in two. Its core knows nothing about hl2sdk or MySQL and is verified on any machine in a second:
cd metamod && make -f Makefile.tests -j8 && ./build-tests/ch_testsBuilding the plugin itself needs hl2sdk, Metamod:Source and a static MariaDB client — that lives in CI. Details: metamod/README.md.
GeoLite2 data by MaxMind, distributed under the GeoLite2 End User License Agreement.
{ "DisplayTimeZone": "UTC", // what players see: "Europe/Moscow", "UTC" or "Local". // The database always stores UTC — this never affects it. "Server": { "Id": 1, // must differ between servers. Was a top-level "ServerId" // before 3.0.1; the old spelling is still read. "PublicAddress": "" // "ip:port" or "host:port". Empty means auto-detect, but // the server process does not know its own public address: // ConVar ip returns the socket bind address, usually 0.0.0.0. }, "Database": { "Host": "127.0.0.1", "SslMode": "Required", // anything but localhost should use this "TablePrefix": "ch_" }, "Collect": { "GeoIp": true, "PlayerIp": true, // personal data — turn off if you don't need it "IpHash": true, "IpHashSalt": "", // empty salt disables hashing (a saltless hash is reversible) "MatchStats": true, "Ping": true, "CountSpectatorTime": true, // false — do not count spectator and // unassigned time as playtime "OnlineSnapshots": true } }