⚡ Enhanced CS2 match management plugin tailored for tournament automation
Enhanced fork of MatchZy tailored for the automatic tournament platform. Adds more events and enables external tools to setup, control, and track matches in real-time.
Use CS2 Server Manager for automated setup with MatchZy Enhanced pre-configured:
👉 Get Started with CS2 Server Manager
- Download the latest release
- Extract to
game/csgo/directory - Restart your server
Built for MatchZy Auto Tournament with extended APIs and events for tournament automation:
- 📡 Extended event system for real-time match tracking
- 🔧 Match report API with structured JSON state
- 🔄 Thread-safe operations for reliable automation
- 🤖 Simulation mode for testing and demos
- 🔁 Event retry system with automatic queue and recovery
- 📊 Server tracking with health monitoring and status events
- 💾 Pull API for direct match stats retrieval
matchzy_loadmatch_url (and matchzy match load) sent while the current series is in postgame does not load right away. The match is queued and loads after the series resets. The reply ends with queued_match=<id>, where <id> is the config file name without its extension (for /api/matches/r2m1.json that is r2m1), and the matchzy_tournament_next_match convar holds the same id. Sending another URL while one is queued replaces it.
The queued match is loaded only by the automatic reset after a series ends. It is dropped when:
css_restartorcss_endmatchresets the server. The reply includescleared_queued_match=<id>.matchzy_clear_queued_matchis run. This is server console / RCON only. The reply iscleared_queued_match=<id>, orcleared_queued_match=nonewhen nothing was queued.
A controller such as MatchZy Auto Tournament points a server at its bootstrap endpoint with two server console / RCON commands:
matchzy_bootstrap_token "<token>"
matchzy_bootstrap_url "http://<controller>/api/servers/<server_id>/bootstrap"
The plugin then fetches that URL (token sent as X-MatchZy-Token) and runs the commands in the
payload. It fetches about 1.5 seconds after the last change to either value, not the moment
one of them is set: every change restarts the timer, and the fetch uses the URL and token current
when it fires. The two commands can be sent in either order and result in one fetch. On startup
the persisted URL and token are fetched straight away.
If the payload sets a matchzy_server_id that differs from the id in the bootstrap URL, or from
the id the server already had, a [Bootstrap] WARNING is logged. The payload is still applied.
This usually means the bootstrap URL is stale.
Server logs, console output and chat never show secret values. The bootstrap, match and report
tokens, remote log / demo upload / backup header values, sv_password, rcon_password and any
other *token*, *password*, *secret* or *header_value* setting are logged as
(hidden, N chars). The same goes for those values inside logged payloads, match configs, HTTP
responses, request headers and URL query strings (?token=). Logs are safe to share when asking
for help.
Older versions printed the token when saving it, e.g.
[SaveConfigValue] Saved config for server '...': matchzy_bootstrap_token = <token>. If you
shared logs from an older version, rotate the MAT SERVER_TOKEN and push the new token to your
servers.
Several servers can point at the same MySQL database. That is the point of a shared stats database, and it now works for persistent config too.
Everything MatchZy persists — the matchzy_server_config table and the event retry queue — is
stored against an identity for the server that wrote it, so one server can no longer overwrite
another's values. Before this, whichever server wrote last won, and on restart every server on the
box loaded that one server's matchzy_server_id, bootstrap URL and remote log settings.
The settings that are now per server, i.e. each server keeps its own value:
matchzy_server_idmatchzy_bootstrap_url,matchzy_bootstrap_tokenmatchzy_remote_log_url,matchzy_remote_log_header_key,matchzy_remote_log_header_valuematchzy_webhook_url,matchzy_heartbeat_urlmatchzy_report_endpoint,matchzy_report_token,matchzy_match_tokenmatchzy_demo_upload_urlmatchzy_admins_url,matchzy_admins_refresh_secondsmatchzy_chat_prefix,matchzy_admin_chat_prefix- all
matchzy_warmup_*settings
The chat prefixes and the warmup settings are usually the same on every server, but they are scoped the same way as the rest: one shared row for them was only ever an accident of the old storage, and "last writer wins" is not a useful way to share a value. Set them per server, or leave the existing shared value in place (see backwards compatibility below).
Genuinely global data — match, map and player stats in matchzy_stats_* — is untouched and stays
shared, which is why you point several servers at one database in the first place.
How a server identifies itself. The identity is derived from the bind address and the game
port, e.g. cs2:27015, cs2:27025, cs2:27035 for three servers on a box named cs2. The bind
address is used when it names a real interface; CS2 servers are nearly always started with
-ip 0.0.0.0, which identifies nothing, so the machine name is used instead. Nothing has to be
configured for this to work, including before a controller like MAT has ever talked to the server.
Two things change a server's identity: changing its game port, and renaming the box. Neither loses data — the server simply finds no row of its own and falls back to the shared pre-upgrade row, and a controller re-pushes its values on the next configure. To pin a name that survives both, set an explicit scope:
# in the server's start arguments (reliable: config.cfg may not have executed yet)
+matchzy_config_scope tournament-eu-3
matchzy_config_scope can also go in config.cfg, but the start-argument form is the one to
prefer and wins when both are set. It is never persisted to the database — a value that decides
which rows you read cannot itself be read from those rows.
The resolved scope is logged once at startup, e.g.
[ConfigScope] Using scope 'cs2-server-2' (from start argument). The order is: the
+matchzy_config_scope start argument, the matchzy_config_scope convar, -port in the start
arguments, then the hostport convar once the server has activated. Start arguments are read from
/proc/self/cmdline on Linux. If none of these identify the server, MatchZy uses a key derived
from the server's install path (still distinct per server, never a key shared by the whole box)
and logs a warning — add +matchzy_config_scope when you see it.
Upgrading from 1.4.26. 1.4.26 could not read the start arguments inside the game process and
resolved every server on a box to the same <host>:27015 scope, so those rows hold whichever
server wrote last. They are left in place but no longer read by any server that resolves a
different scope (reads only ever fall back to the pre-scoping shared row, never to another
scope). A controller re-pushes the correct values on the next configure. Once every server logs
its own scope you can remove the stale rows, e.g.
DELETE FROM matchzy_server_config WHERE server_scope = 'cs2:27015'; — but only if no server on
that box legitimately resolves to that scope (a server without +matchzy_config_scope on port
27015 does).
Backwards compatibility. Rows written before this change are kept and treated as shared fallbacks. A server reads its own row when it has one and the shared row otherwise, and only ever writes its own row. So one server per database keeps working with no operator action, and a multi-server setup keeps its current behaviour until each server writes its own values. The schema migration runs automatically on startup and is a no-op once applied.
If you worked around this by moving servers to per-server SQLite files, you can move them back to the shared MySQL database.
- 🚀 Auto-ready system — Instant match starts (optional)
- ⏸️ Enhanced pauses — Team limits, timeouts, dual unpause
- ⏱️ Side selection timer — Auto-decide after knife round
- 🏳️
.ggcommand — Team vote to forfeit early - 🚫 FFW system — Handle full team disconnects
- ⚡ Smart demo delays — 10s restart when demos disabled
- 📺 Center notifications — Important events shown center-screen with countdown timers
- 📋 Configuration Guide — All ConVars and examples
- 🎮 Commands Reference — Player and admin commands
- 🔗 Integration Guide — API endpoints and events
- 📝 Changelog — Release history
- MatchZy Auto Tournament — Automated tournament platform
- CS2 Server Manager — Multi-server deployment tool
Original MatchZy: shobhit-pathak/MatchZy by WD-
Enhanced Fork: Maintained by sivert-io for MatchZy Auto Tournament
Built with CounterStrikeSharp • Inspired by Get5
Made with ❤️ for the CS2 community
release.sh bumps ModuleVersion, builds, commits, tags and publishes the
zip. On GitHub it runs from the Release workflow: the Run workflow
button with a bump type, or -- for an environment that can push branches but
not press the button -- a push of a branch named release/patch,
release/minor or release/major. Either way the release is cut on dev;
the branch is only the trigger and the job deletes it when it is done.
This is cs2-xpbot's fork of
MatchZy-Enhanced. It tracks upstream dev; the branch xpbot carries the
changes below on top, each written to be offered upstream.
- The game can no longer change map before MatchZy does.
HandleMatchEndraisesmp_match_restart_delayto cover the delay it chose in every branch, not only when a demo upload URL is configured. Before this, a server recording without an upload URL waitedtv_delay + 25s against the game's 25 s default, and the game loaded the next map of the map group first, with the demo still being written. - One
.readyper team. A loaded match goes live once one player on each team has typed.ready(and the rosters are full), not once every player has. A config'smin_players_to_readystill overrides it,0meaning everyone on the team, and!readyrequiredstill sets it from the console. The unready reminder names only the teams still holding the match up. - The damage report counts health taken, not weapon damage. CS2's
player_hurtreports what the weapon dealt, so an AWP shot on a player at 40 HP was reported as 100+ and a report could say 140 in 1 hit. Each hit is now measured against the health the victim had, so a player's damage to one opponent in a round never exceeds 100. - The Steam update check is off by default (
matchzy_safeautoupdater_enabled). cs2-server-agent owns CS2 updates for the whole host, xpbot ignores the events the check emits, and a LAN box without internet only logged an offline warning from it every half hour. The rest of upstream's Auto Tournament integration (heartbeat, bootstrap, admins refresh, match report) is left in place and stays inert until its URL is configured, so upstream fixes keep merging cleanly. - Nothing waits on the network or the database on the game thread. The
match config (
matchzy_loadmatch_url), a queued match and a backup restore from a URL were fetched with a blocking call on the game thread and HttpClient's 100 s default timeout: with the control plane down, the whole server froze until it ran out. They are now fetched on the thread pool with a 30 s timeout and applied on the next frame, and a second load requested while one is in flight is refused rather than raced. The event retry loop's queue read and the health endpoint's database check run on the pool too. - A health endpoint. From v1.4.34 the plugin answers
GET /healthwith its version, config scope, uptime, map, the match it has loaded and its status, its database check, its event queue and a verdict of its own, so a control plane can ask the plugin whether it is loaded and working instead of reading the server's journal for aLOADEDline. It listens on the abstract Unix socket@matchzy-health-<scope>from the moment it loads, with no configuration (curl --abstract-unix-socket matchzy-health-<scope> http://matchzy/healthon the host; the scope ismatchzy_config_scope, orhost:portwithout one), and on TCP whenmatchzy_health_portnames a port — bound to127.0.0.1, or tomatchzy_health_bind. The report is captured on the game thread every 2 s and served from a background thread, sosnapshot_age_secondspast 20 means the game thread has stopped: the plugin reports that, and a database it cannot reach, asproblemswithok: false. The reply carries no secrets. - Open rosters. A match config with
"open_rosters": truelets a team be sent by name alone: a team whoseplayersobject is empty takes its players from whoever joins its side, up toplayers_per_team. A player on no roster is no longer kicked while an open team still has a place; choosing that team's side writes them into its roster (<name> joins <team>in chat), and from then on they are that team's player as if the config had named them — side swaps, the knife round, reconnects and the stats all follow the roster. Until the match is live a player may move to the other team if it is open too. A team that did send players stays locked to them, and without the flag an empty roster kicks everyone, as before. css_forcewin <team1|team2>ends the map being played with the given team as its winner and lets the normal match-end path run: series score, clinch, demo and map change.css_endmatchdrops the whole loaded series; this ends one map of it. Admin or console/RCON only; needs a live map.- Demos are recorded again after the CS2 update of 2026-09-22. Since that
update a relative path given to
tv_recordlands in the firstGamesearch path ofgameinfo.gi, which on a Metamod server iscsgo/addons/metamod/.tv_record MatchZy/<name>.demtherefore tried to open a file in a directory that does not exist, the engine wrote nothing and said so only on the console, and the plugin found out at upload time (file_not_found). Ported from upstream's 1.4.35 hotfix:tv_recordgets the absolute<game>/csgo/<demo path><name>, the demo name is built from ASCII letters, digits,-,_and.only (workshop map names and team names with/ ; "or symbols broke the path or the console command, and non-ASCII names could never be uploaded), and the upload's fallback search also looks undercsgo/addons/metamod/, so demos older builds recorded there are still found. The engine's round backups (mp_backup_round_file) moved the same way and take a prefix rather than a path, so the plugin now reads the Valve backup from whichever ofcsgo/andcsgo/addons/metamod/holds it instead of storing an emptyvalve_backup.