/* * CSB Mix ELO * Copyright (C) 2026 counter-strike-boost.com * * ELO rating for mix/match play stored in MySQL. Ratings load asynchronously on * authorise; after a match amx_elo_result applies the standard ELO update * (winners gain, losers lose, scaled by the rating gap and K factor) and writes * back through threaded SQLx. /elo shows your rating, /topelo the ladder, and * amx_elobalance prints ELO-balanced teams. * * This program is free software: you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any later * version. It is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See for details. */ #include #include #include new const PLUGIN[] = "CSB Mix ELO" new const VERSION[] = "1.0.0" new const AUTHOR[] = "counter-strike-boost.com" new const TABLE[] = "csb_elo" new g_pHost, g_pUser, g_pPass, g_pDb, g_pK, g_pStart new Handle:g_hTuple = Empty_Handle new g_iRating[33] new bool:g_bLoaded[33] new g_szAuth[33][35] public plugin_init() { register_plugin(PLUGIN, VERSION, AUTHOR) g_pHost = register_cvar("csb_elo_host", "127.0.0.1") g_pUser = register_cvar("csb_elo_user", "csb") g_pPass = register_cvar("csb_elo_pass", "") g_pDb = register_cvar("csb_elo_db", "csb") g_pK = register_cvar("csb_elo_k", "32") g_pStart = register_cvar("csb_elo_start", "1000") register_clcmd("say /elo", "cmdElo") register_clcmd("say_team /elo", "cmdElo") register_clcmd("say /topelo", "cmdTop") register_clcmd("say_team /topelo", "cmdTop") register_concmd("amx_elo_result", "cmdResult", ADMIN_BAN, " - apply match result to ELO") register_concmd("amx_elobalance", "cmdBalance", ADMIN_BAN, "- print ELO-balanced teams") } public plugin_cfg() { new host[64], user[64], pass[64], db[64] get_pcvar_string(g_pHost, host, charsmax(host)) get_pcvar_string(g_pUser, user, charsmax(user)) get_pcvar_string(g_pPass, pass, charsmax(pass)) get_pcvar_string(g_pDb, db, charsmax(db)) if (host[0] && db[0]) { g_hTuple = SQL_MakeDbTuple(host, user, pass, db) new q[512] formatex(q, charsmax(q), "CREATE TABLE IF NOT EXISTS `%s` (`authid` VARCHAR(34) NOT NULL PRIMARY KEY, `name` VARCHAR(32) NOT NULL DEFAULT '', `rating` INT NOT NULL DEFAULT 1000, `wins` INT UNSIGNED NOT NULL DEFAULT 0, `losses` INT UNSIGNED NOT NULL DEFAULT 0, `games` INT UNSIGNED NOT NULL DEFAULT 0, KEY `k_rating` (`rating`)) ENGINE=InnoDB", TABLE) SQL_ThreadQuery(g_hTuple, "handlerSimple", q) } } public plugin_end() { if (g_hTuple != Empty_Handle) SQL_FreeHandle(g_hTuple) } public client_authorized(id) { g_bLoaded[id] = false g_iRating[id] = get_pcvar_num(g_pStart) g_szAuth[id][0] = 0 if (is_user_bot(id) || is_user_hltv(id) || g_hTuple == Empty_Handle) return get_user_authid(id, g_szAuth[id], charsmax(g_szAuth[])) if (!g_szAuth[id][0]) return new safe[70] sqlEscape(g_szAuth[id], safe, charsmax(safe)) new q[192] formatex(q, charsmax(q), "SELECT `rating` FROM `%s` WHERE `authid` = '%s'", TABLE, safe) new data[2] data[0] = id data[1] = get_user_userid(id) SQL_ThreadQuery(g_hTuple, "handlerLoad", q, data, sizeof(data)) } public handlerLoad(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) { if (failstate != TQUERY_SUCCESS) { log_amx("[CSB ELO] load failed (%d): %s", errnum, error) return } new id = data[0] if (!is_user_connected(id) || get_user_userid(id) != data[1]) return if (SQL_NumResults(query)) g_iRating[id] = SQL_ReadResult(query, 0) g_bLoaded[id] = true } public cmdElo(id) { client_print(id, print_chat, "[CSB] Your ELO rating is %d.", g_iRating[id]) return PLUGIN_HANDLED } public cmdTop(id) { if (g_hTuple == Empty_Handle) { client_print(id, print_chat, "[CSB] The ELO database is not configured.") return PLUGIN_HANDLED } new data[2] data[0] = id data[1] = get_user_userid(id) SQL_ThreadQuery(g_hTuple, "handlerTop", "SELECT `name`, `rating` FROM `csb_elo` ORDER BY `rating` DESC LIMIT 10", data, sizeof(data)) return PLUGIN_HANDLED } public handlerTop(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) { if (failstate != TQUERY_SUCCESS) { log_amx("[CSB ELO] top query failed (%d): %s", errnum, error) return } new id = data[0] if (!is_user_connected(id) || get_user_userid(id) != data[1]) return client_print(id, print_chat, "[CSB] ===== Top ELO =====") new rank = 0 while (SQL_MoreResults(query)) { rank++ new name[32] SQL_ReadResult(query, 0, name, charsmax(name)) new rating = SQL_ReadResult(query, 1) client_print(id, print_chat, "[CSB] %d. %s - %d", rank, name, rating) SQL_NextRow(query) } if (!rank) client_print(id, print_chat, "[CSB] No rated players yet.") } public cmdResult(id, level, cid) { if (!cmd_access(id, level, cid, 2)) return PLUGIN_HANDLED new arg[12] read_argv(1, arg, charsmax(arg)) new winnerTeam if (equali(arg, "CT")) winnerTeam = 2 else if (equali(arg, "T") || equali(arg, "TERRORIST")) winnerTeam = 1 else { console_print(id, "[CSB] Usage: amx_elo_result ") return PLUGIN_HANDLED } applyResult(winnerTeam) console_print(id, "[CSB] ELO updated for the match.") return PLUGIN_HANDLED } applyResult(winnerTeam) { new players[32], num get_players(players, num, "ch") new sumW = 0, cntW = 0, sumL = 0, cntL = 0 for (new i = 0; i < num; i++) { new t = get_user_team(players[i]) if (t == winnerTeam) { sumW += g_iRating[players[i]] cntW++ } else if (t == 1 || t == 2) { sumL += g_iRating[players[i]] cntL++ } } if (!cntW || !cntL) { client_print(0, print_chat, "[CSB] Both teams need players to rate a result.") return } new Float:avgW = float(sumW) / float(cntW) new Float:avgL = float(sumL) / float(cntL) new Float:k = get_pcvar_float(g_pK) new Float:expW = 1.0 / (1.0 + floatpower(10.0, (avgL - avgW) / 400.0)) new Float:expL = 1.0 / (1.0 + floatpower(10.0, (avgW - avgL) / 400.0)) new deltaW = floatround(k * (1.0 - expW)) new deltaL = floatround(k * (0.0 - expL)) for (new i = 0; i < num; i++) { new pid = players[i] new t = get_user_team(pid) if (t != 1 && t != 2) continue new bool:won = (t == winnerTeam) new delta = won ? deltaW : deltaL g_iRating[pid] += delta if (g_iRating[pid] < 100) g_iRating[pid] = 100 saveRating(pid, won) client_print(pid, print_chat, "[CSB] Match %s! Your ELO is now %d (%s%d).", won ? "won" : "lost", g_iRating[pid], delta >= 0 ? "+" : "", delta) } } saveRating(id, bool:won) { if (g_hTuple == Empty_Handle || !g_szAuth[id][0]) return new name[32], safeName[70], safeAuth[70] get_user_name(id, name, charsmax(name)) sqlEscape(name, safeName, charsmax(safeName)) sqlEscape(g_szAuth[id], safeAuth, charsmax(safeAuth)) new q[600] formatex(q, charsmax(q), "INSERT INTO `%s` (`authid`, `name`, `rating`, `wins`, `losses`, `games`) VALUES ('%s', '%s', %d, %d, %d, 1) ON DUPLICATE KEY UPDATE `name` = VALUES(`name`), `rating` = %d, `wins` = `wins` + %d, `losses` = `losses` + %d, `games` = `games` + 1", TABLE, safeAuth, safeName, g_iRating[id], won ? 1 : 0, won ? 0 : 1, g_iRating[id], won ? 1 : 0, won ? 0 : 1) SQL_ThreadQuery(g_hTuple, "handlerSimple", q) } public cmdBalance(id, level, cid) { if (!cmd_access(id, level, cid, 1)) return PLUGIN_HANDLED new players[32], num get_players(players, num, "ch") /* insertion sort by rating, descending */ for (new i = 1; i < num; i++) { new key = players[i] new j = i - 1 while (j >= 0 && g_iRating[players[j]] < g_iRating[key]) { players[j + 1] = players[j] j-- } players[j + 1] = key } new teamA[512], teamB[512] teamA[0] = 0 teamB[0] = 0 new sumA = 0, sumB = 0 for (new i = 0; i < num; i++) { new pid = players[i] new name[32] get_user_name(pid, name, charsmax(name)) new entry[48] formatex(entry, charsmax(entry), "%s(%d) ", name, g_iRating[pid]) if (sumA <= sumB) { add(teamA, charsmax(teamA), entry) sumA += g_iRating[pid] } else { add(teamB, charsmax(teamB), entry) sumB += g_iRating[pid] } } console_print(id, "[CSB ELO] Team A (total %d): %s", sumA, teamA) console_print(id, "[CSB ELO] Team B (total %d): %s", sumB, teamB) client_print(id, print_chat, "[CSB] ELO-balanced teams printed to your console.") return PLUGIN_HANDLED } public handlerSimple(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) { if (failstate != TQUERY_SUCCESS) log_amx("[CSB ELO] query failed (%d): %s", errnum, error) } stock sqlEscape(const input[], output[], len) { new i = 0, j = 0, c while (input[i] != 0 && j < len) { c = input[i] if (c == 39 || c == 34 || c == 92 || c == 59 || c == 37 || c == 96 || c < 32) { i++ continue } output[j++] = c i++ } output[j] = 0 }