Простой плагин для CounterStrikeSharp, который позволяет VIP-игрокам использовать автоматический бхоп на сервере CS2. работает напрямую с VIP CORE
Исходный код плагина:
Команды
- !bhop - включить/выключить бхоп (только для VIP)
- !checkvip - проверить VIP статус
- !bhopcheck - принудительная проверка VIP (если выдали во время игры)
Установка
- Скопировать файлы из Bhopvip_CS2-main/bin/Release/net8.0/ в папку плагинов: /addons/counterstrikesharp/plugins/BhopVip/
- Настроить config.json (БД, ServerID, язык)
- Перезагрузить плагины: cssharp plugins reload
Поддерживаемые языки
- Українська (ua)
- Русский (ru)
- English (en)
Исходный код плагина:
C-подобный:
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Modules.Utils;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Timers;
using MySqlConnector;
using System.Text.Json;
namespace BhopVip;
public class BhopVip : BasePlugin
{
public override string ModuleName => "BhopVip";
public override string ModuleVersion => "1.0.1";
public override string ModuleAuthor => "Alley";
public override string ModuleDescription => "VIP Per-Player Auto Bhop";
private const ulong SteamId64Base = 76561197960265728UL;
private const float BhopJumpVelocity = 290f;
private class DbConfig
{
public string Host { get; set; } = "127.0.0.1";
public int Port { get; set; } = 3306;
public string Database { get; set; } = "cs2";
public string User { get; set; } = "root";
public string Password { get; set; } = "";
}
private class PluginConfig
{
public DbConfig Database { get; set; } = new();
public int ServerID { get; set; } = 0;
public bool RoundDelayEnabled { get; set; } = true;
public int RoundDelaySeconds { get; set; } = 5;
public bool DamageCooldownEnabled { get; set; } = false;
public int DamageCooldownSeconds { get; set; } = 3;
public List<string> AllowedGroups { get; set; } = new() { "vip" };
public string Language { get; set; } = "ru";
public bool Debug { get; set; } = false;
}
private PluginConfig _config = new();
private MySqlConnection? _db;
private HashSet<uint> _activeVips = new();
private Dictionary<uint, bool> _playerVipStatus = new();
private Dictionary<uint, string> _playerGroup = new();
private Dictionary<uint, CounterStrikeSharp.API.Modules.Timers.Timer> _damageCooldownTimers = new();
private bool _roundActive = false;
private string ConfigPath => Path.Combine(ModuleDirectory, "../../configs/plugins", ModuleName, "config.json");
private string LangDir => Path.Combine(ModuleDirectory, "../../lang/bhopvip");
private Dictionary<string, string> _translations = new();
public override void Load(bool hotReload)
{
CreateConfigIfNotExists();
LoadConfig();
CreateLanguageFiles();
LoadTranslations();
ConnectDatabase();
Server.ExecuteCommand("sv_autobunnyhopping 1");
Server.ExecuteCommand("sv_enablebunnyhopping 1");
Server.ExecuteCommand("sv_airaccelerate 1000");
Server.ExecuteCommand("sv_maxvelocity 7000");
Server.ExecuteCommand("sv_staminamax 0");
Server.ExecuteCommand("sv_staminalandcost 0");
Server.ExecuteCommand("sv_staminajumpcost 0");
RegisterListener<Listeners.OnTick>(OnTick);
RegisterEventHandler<EventPlayerConnectFull>(OnPlayerConnectFull);
RegisterEventHandler<EventPlayerDisconnect>(OnPlayerDisconnect);
RegisterEventHandler<EventRoundStart>(OnRoundStart);
RegisterEventHandler<EventPlayerHurt>(OnPlayerHurt);
RegisterEventHandler<EventPlayerSpawn>(OnPlayerSpawn);
AddCommand("bhop", "Toggle bunnyhop for VIPs", OnBhopCommand);
AddCommand("checkvip", "Check VIP status", OnCheckVipCommand);
AddCommand("bhopcheck", "Force check VIP status", OnBhopCheckCommand);
AddTimer(60.0f, CheckAllPlayersVip, TimerFlags.REPEAT);
AddTimer(30.0f, EnsureDbConnection, TimerFlags.REPEAT);
Console.WriteLine($"[{ModuleName}] Loaded v{ModuleVersion}");
}
private void OnTick()
{
if (!_roundActive) return;
foreach (var player in Utilities.GetPlayers())
{
if (player == null || !player.IsValid || player.IsBot || !player.PawnIsAlive) continue;
uint accId = (uint)(player.SteamID - SteamId64Base);
if (!_activeVips.Contains(accId) || _damageCooldownTimers.ContainsKey(accId)) continue;
var pawn = player.PlayerPawn.Value;
if (pawn == null) continue;
var flags = (PlayerFlags)pawn.Flags;
if (flags.HasFlag(PlayerFlags.FL_ONGROUND) &&
player.Buttons.HasFlag(PlayerButtons.Jump) &&
pawn.AbsVelocity.Z <= 8.0f)
{
pawn.AbsVelocity.Z = BhopJumpVelocity;
}
}
}
private void CreateConfigIfNotExists()
{
var dir = Path.GetDirectoryName(ConfigPath);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir!);
if (!File.Exists(ConfigPath))
{
var defaultConfig = new PluginConfig();
string json = JsonSerializer.Serialize(defaultConfig, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(ConfigPath, json);
Console.WriteLine($"[{ModuleName}] Created default config at {ConfigPath}");
}
}
private void LoadConfig()
{
try
{
string json = File.ReadAllText(ConfigPath);
_config = JsonSerializer.Deserialize<PluginConfig>(json) ?? new PluginConfig();
}
catch (Exception ex)
{
Console.WriteLine($"[{ModuleName}] Config error: {ex.Message}");
}
}
private void CreateLanguageFiles()
{
if (!Directory.Exists(LangDir))
Directory.CreateDirectory(LangDir);
var languages = new[] { "en", "ru", "ua" };
foreach (var lang in languages)
{
var file = Path.Combine(LangDir, $"{lang}.json");
if (!File.Exists(file))
{
var translations = lang switch
{
"en" => new Dictionary<string, string>
{
["vip_yes_bhop_on"] = "[VIP] You have VIP status! Bunnyhop automatically enabled",
["bhop_on"] = "[VIP] Bunnyhop ENABLED",
["bhop_off"] = "[VIP] Bunnyhop DISABLED",
["only_vip"] = "[VIP] Only VIP can use !bhop",
["vip_check_yes"] = "[VIP] You have VIP status! Bunnyhop: {status}",
["vip_check_no"] = "[VIP] You don't have VIP status!",
["vip_found"] = "[VIP] Congratulations! You got VIP status! Bunnyhop automatically enabled",
["bhop_status_on"] = "ENABLED",
["bhop_status_off"] = "DISABLED",
["db_error"] = "[VIP] Database connection error",
["round_delay_wait"] = "[VIP] Bunnyhop will be available in {delay} seconds",
["round_delay_active"] = "[VIP] Bunnyhop is now available!",
["damage_cooldown_start"] = "[VIP] Bunnyhop disabled for {seconds} seconds due to damage!",
["damage_cooldown_end"] = "[VIP] Bunnyhop re-enabled!"
},
"ru" => new Dictionary<string, string>
{
["vip_yes_bhop_on"] = "[VIP] У вас есть VIP статус! Бхоп автоматически включён",
["bhop_on"] = "[VIP] Бхоп ВКЛЮЧЁН",
["bhop_off"] = "[VIP] Бхоп ВЫКЛЮЧЕН",
["only_vip"] = "[VIP] Только VIP могут использовать !bhop",
["vip_check_yes"] = "[VIP] У вас есть VIP статус! Бхоп: {status}",
["vip_check_no"] = "[VIP] У вас нет VIP статуса!",
["vip_found"] = "[VIP] Поздравляем! Вы получили VIP статус! Бхоп автоматически включён",
["bhop_status_on"] = "ВКЛЮЧЁН",
["bhop_status_off"] = "ВЫКЛЮЧЕН",
["db_error"] = "[VIP] Ошибка подключения к БД",
["round_delay_wait"] = "[VIP] Бхоп станет доступен через {delay} секунд",
["round_delay_active"] = "[VIP] Бхоп теперь доступен!",
["damage_cooldown_start"] = "[VIP] Бхоп отключён на {seconds} секунд из-за урона!",
["damage_cooldown_end"] = "[VIP] Бхоп снова включён!"
},
_ => new Dictionary<string, string>
{
["vip_yes_bhop_on"] = "[VIP] У вас є VIP статус! Бхоп автоматично включений",
["bhop_on"] = "[VIP] Бхоп УВІМКНЕНО",
["bhop_off"] = "[VIP] Бхоп ВИМКНЕНО",
["only_vip"] = "[VIP] Тільки VIP можуть використовувати !bhop",
["vip_check_yes"] = "[VIP] У вас є VIP статус! Бхоп: {status}",
["vip_check_no"] = "[VIP] У вас немає VIP статусу!",
["vip_found"] = "[VIP] Вітаю! Ви отримали VIP статус! Бхоп автоматично включений",
["bhop_status_on"] = "УВІМКНЕНО",
["bhop_status_off"] = "ВИМКНЕНО",
["db_error"] = "[VIP] Помилка підключення до БД",
["round_delay_wait"] = "[VIP] Бхоп стане доступним через {delay} секунд",
["round_delay_active"] = "[VIP] Бхоп тепер доступний!",
["damage_cooldown_start"] = "[VIP] Бхоп вимкнено на {seconds} секунд через пошкодження!",
["damage_cooldown_end"] = "[VIP] Бхоп знову включено!"
}
};
string json = JsonSerializer.Serialize(translations, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(file, json);
}
}
}
private void LoadTranslations()
{
string lang = _config.Language;
if (!new[] { "en", "ru", "ua" }.Contains(lang))
lang = "ru";
var file = Path.Combine(LangDir, $"{lang}.json");
if (File.Exists(file))
{
try
{
string json = File.ReadAllText(file);
var dict = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
if (dict != null) _translations = dict;
}
catch (Exception ex)
{
Console.WriteLine($"[{ModuleName}] Failed to load language file: {ex.Message}");
}
}
}
private string Localize(string key, Dictionary<string, string>? replacements = null)
{
if (!_translations.TryGetValue(key, out var text))
return key;
if (replacements != null)
{
foreach (var rep in replacements)
text = text.Replace($"{{{rep.Key}}}", rep.Value);
}
return text;
}
private void ConnectDatabase()
{
try
{
var builder = new MySqlConnectionStringBuilder
{
Server = _config.Database.Host,
Port = (uint)_config.Database.Port,
Database = _config.Database.Database,
UserID = _config.Database.User,
Password = _config.Database.Password,
ConnectionTimeout = 5,
SslMode = MySqlSslMode.None,
Pooling = true
};
_db = new MySqlConnection(builder.ConnectionString);
_db.Open();
Console.WriteLine($"[{ModuleName}] DB connected");
}
catch (Exception ex)
{
Console.WriteLine($"[{ModuleName}] DB connection failed: {ex.Message}");
}
}
private void EnsureDbConnection()
{
if (_db != null && _db.State == System.Data.ConnectionState.Open)
return;
ConnectDatabase();
if (_db?.State == System.Data.ConnectionState.Open)
CheckAllPlayersVip();
}
private (bool isVip, string group)? GetVipInfoFromDb(uint accountId)
{
if (_db?.State != System.Data.ConnectionState.Open) return null;
try
{
using var cmd = new MySqlCommand("SELECT `group`, `expires` FROM `vip_users` WHERE `account_id` = @id AND `sid` = @sid LIMIT 1", _db);
cmd.Parameters.AddWithValue("@id", accountId);
cmd.Parameters.AddWithValue("@sid", _config.ServerID);
cmd.CommandTimeout = 5;
using var reader = cmd.ExecuteReader();
if (reader.Read())
{
string group = reader.GetString(0);
long expires = reader.GetInt64(1);
bool validExpires = expires == 0 || expires > DateTimeOffset.UtcNow.ToUnixTimeSeconds();
bool groupAllowed = _config.AllowedGroups.Contains(group);
return (validExpires && groupAllowed, group);
}
}
catch (Exception ex)
{
Console.WriteLine($"[{ModuleName}] SQL error: {ex.Message}");
}
return null;
}
private void CheckAllPlayersVip()
{
if (_db?.State != System.Data.ConnectionState.Open) return;
foreach (var player in Utilities.GetPlayers())
{
if (player == null || player.SteamID == 0) continue;
uint accId = (uint)(player.SteamID - SteamId64Base);
var info = GetVipInfoFromDb(accId);
bool isVip = info?.isVip ?? false;
_playerVipStatus[accId] = isVip;
if (info != null) _playerGroup[accId] = info.Value.group;
if (isVip && !_activeVips.Contains(accId) && _roundActive && !_damageCooldownTimers.ContainsKey(accId))
{
_activeVips.Add(accId);
player.PrintToChat(Localize("vip_found"));
}
else if (!isVip && _activeVips.Contains(accId))
{
_activeVips.Remove(accId);
}
SyncPlayerBhop(player, accId);
}
}
private void ApplyBhopState(CCSPlayerController? player, bool enabled)
{
if (player == null || !player.IsValid || player.Connected != PlayerConnectedState.PlayerConnected) return;
try
{
player.ReplicateConVar("sv_autobunnyhopping", enabled ? "1" : "0");
player.ReplicateConVar("sv_enablebunnyhopping", enabled ? "1" : "0");
}
catch (Exception ex)
{
if (_config.Debug)
Console.WriteLine($"[{ModuleName}] ReplicateConVar failed for {player.PlayerName}: {ex.Message}");
}
}
private void SyncPlayerBhop(CCSPlayerController? player, uint accId)
{
bool shouldBhop = _activeVips.Contains(accId) &&
_playerVipStatus.GetValueOrDefault(accId) &&
_roundActive &&
!_damageCooldownTimers.ContainsKey(accId);
ApplyBhopState(player, shouldBhop);
}
private HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info)
{
var player = @event.Userid;
if (player?.SteamID == 0) return HookResult.Continue;
uint accId = (uint)(player!.SteamID - SteamId64Base);
AddTimer(0.25f, () =>
{
if (player.IsValid)
SyncPlayerBhop(player, accId);
}, TimerFlags.STOP_ON_MAPCHANGE);
return HookResult.Continue;
}
private void StartDamageCooldown(CCSPlayerController player, uint accId)
{
if (!_config.DamageCooldownEnabled) return;
if (!_playerVipStatus.GetValueOrDefault(accId)) return;
if (_damageCooldownTimers.TryGetValue(accId, out var existing))
{
existing.Kill();
_damageCooldownTimers.Remove(accId);
}
if (_activeVips.Contains(accId))
{
_activeVips.Remove(accId);
}
player.PrintToChat(Localize("damage_cooldown_start", new Dictionary<string, string> { ["seconds"] = _config.DamageCooldownSeconds.ToString() }));
ApplyBhopState(player, false);
var newTimer = AddTimer(_config.DamageCooldownSeconds, () =>
{
_damageCooldownTimers.Remove(accId);
if (!player.IsValid) return;
if (_playerVipStatus.GetValueOrDefault(accId) && _roundActive && !_activeVips.Contains(accId))
{
_activeVips.Add(accId);
player.PrintToChat(Localize("damage_cooldown_end"));
}
SyncPlayerBhop(player, accId);
}, TimerFlags.STOP_ON_MAPCHANGE);
_damageCooldownTimers[accId] = newTimer;
}
private HookResult OnPlayerHurt(EventPlayerHurt @event, GameEventInfo info)
{
var victim = @event.Userid;
if (victim?.SteamID == 0) return HookResult.Continue;
uint accId = (uint)(victim!.SteamID - SteamId64Base);
StartDamageCooldown(victim, accId);
return HookResult.Continue;
}
private HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info)
{
_roundActive = false;
_activeVips.Clear();
foreach (var p in Utilities.GetPlayers())
ApplyBhopState(p, false);
if (!_config.RoundDelayEnabled)
{
_roundActive = true;
CheckAllPlayersVip();
return HookResult.Continue;
}
foreach (var player in Utilities.GetPlayers())
{
if (player?.SteamID == 0) continue;
if (_playerVipStatus.GetValueOrDefault((uint)(player!.SteamID - SteamId64Base)))
player.PrintToChat(Localize("round_delay_wait", new Dictionary<string, string> { ["delay"] = _config.RoundDelaySeconds.ToString() }));
}
AddTimer(_config.RoundDelaySeconds, () =>
{
_roundActive = true;
CheckAllPlayersVip();
foreach (var player in Utilities.GetPlayers())
{
if (player?.SteamID == 0) continue;
uint accId = (uint)(player!.SteamID - SteamId64Base);
if (_playerVipStatus.GetValueOrDefault(accId) && !_activeVips.Contains(accId) && !_damageCooldownTimers.ContainsKey(accId))
{
_activeVips.Add(accId);
player.PrintToChat(Localize("round_delay_active"));
}
SyncPlayerBhop(player, accId);
}
}, TimerFlags.STOP_ON_MAPCHANGE);
return HookResult.Continue;
}
private HookResult OnPlayerConnectFull(EventPlayerConnectFull @event, GameEventInfo info)
{
var player = @event.Userid;
if (player?.SteamID == 0) return HookResult.Continue;
uint accId = (uint)(player!.SteamID - SteamId64Base);
var vipInfo = GetVipInfoFromDb(accId);
bool isVip = vipInfo?.isVip ?? false;
_playerVipStatus[accId] = isVip;
if (vipInfo != null) _playerGroup[accId] = vipInfo.Value.group;
if (isVip)
{
if (_roundActive && !_activeVips.Contains(accId) && !_damageCooldownTimers.ContainsKey(accId))
{
_activeVips.Add(accId);
player.PrintToChat(Localize("vip_yes_bhop_on"));
}
else if (!_roundActive)
{
player.PrintToChat(Localize("vip_yes_bhop_on"));
player.PrintToChat(Localize("round_delay_wait", new Dictionary<string, string> { ["delay"] = _config.RoundDelaySeconds.ToString() }));
}
}
SyncPlayerBhop(player, accId);
return HookResult.Continue;
}
private HookResult OnPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo info)
{
var player = @event.Userid;
if (player?.SteamID == 0) return HookResult.Continue;
uint accId = (uint)(player!.SteamID - SteamId64Base);
_activeVips.Remove(accId);
if (_damageCooldownTimers.TryGetValue(accId, out var timer))
{
timer.Kill();
_damageCooldownTimers.Remove(accId);
}
_playerVipStatus.Remove(accId);
_playerGroup.Remove(accId);
return HookResult.Continue;
}
private void OnBhopCommand(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
uint accId = (uint)(player.SteamID - SteamId64Base);
if (!_playerVipStatus.GetValueOrDefault(accId))
{
player.PrintToChat(Localize("only_vip"));
return;
}
if (!_roundActive && _config.RoundDelayEnabled)
{
player.PrintToChat(Localize("round_delay_wait", new Dictionary<string, string> { ["delay"] = _config.RoundDelaySeconds.ToString() }));
return;
}
if (_damageCooldownTimers.ContainsKey(accId))
{
player.PrintToChat(Localize("damage_cooldown_start", new Dictionary<string, string> { ["seconds"] = _config.DamageCooldownSeconds.ToString() }));
return;
}
if (_activeVips.Contains(accId))
{
_activeVips.Remove(accId);
player.PrintToChat(Localize("bhop_off"));
}
else
{
_activeVips.Add(accId);
player.PrintToChat(Localize("bhop_on"));
}
SyncPlayerBhop(player, accId);
}
private void OnCheckVipCommand(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
uint accId = (uint)(player.SteamID - SteamId64Base);
bool isVip = _playerVipStatus.GetValueOrDefault(accId);
if (isVip)
{
string status;
if (!_roundActive && _config.RoundDelayEnabled)
status = Localize("bhop_status_off") + " (waiting round)";
else if (_damageCooldownTimers.ContainsKey(accId))
status = Localize("bhop_status_off") + " (cooldown)";
else
status = _activeVips.Contains(accId) ? Localize("bhop_status_on") : Localize("bhop_status_off");
player.PrintToChat(Localize("vip_check_yes", new Dictionary<string, string> { ["status"] = status }));
}
else
player.PrintToChat(Localize("vip_check_no"));
}
private void OnBhopCheckCommand(CCSPlayerController? player, CommandInfo command)
{
if (player == null) return;
uint accId = (uint)(player.SteamID - SteamId64Base);
var vipInfo = GetVipInfoFromDb(accId);
bool isVip = vipInfo?.isVip ?? false;
_playerVipStatus[accId] = isVip;
if (vipInfo != null) _playerGroup[accId] = vipInfo.Value.group;
if (isVip && !_activeVips.Contains(accId) && _roundActive && !_damageCooldownTimers.ContainsKey(accId))
{
_activeVips.Add(accId);
player.PrintToChat(Localize("vip_found"));
SyncPlayerBhop(player, accId);
}
else if (isVip)
{
string status = _activeVips.Contains(accId) ? Localize("bhop_status_on") : Localize("bhop_status_off");
if (!_roundActive && _config.RoundDelayEnabled)
status += " (waiting round)";
else if (_damageCooldownTimers.ContainsKey(accId))
status += " (cooldown)";
player.PrintToChat(Localize("vip_check_yes", new Dictionary<string, string> { ["status"] = status }));
}
else
player.PrintToChat(Localize("vip_check_no"));
}
public override void Unload(bool hotReload)
{
foreach (var p in Utilities.GetPlayers())
ApplyBhopState(p, false);
_db?.Close();
foreach (var timer in _damageCooldownTimers.Values)
timer.Kill();
_damageCooldownTimers.Clear();
_activeVips.Clear();
_playerVipStatus.Clear();
_playerGroup.Clear();
}
}
- Требования
-
Требования
- CounterStrikeSharp
- MySQL / MariaDB
- VIP CORE
И всё.
- Переменные
-
Переменная Описание Database.Host Хост MySQL Database.Port Порт MySQL Database.Database Имя БД Database.User Пользователь MySQL Database.Password Пароль MySQL ServerID ID сервера Language Язык (ua, ru, en)
- Команды
-
Команда Описание !bhop Включить/выключить бхоп для себя !checkvip Проверить VIP статус !bhopcheck Принудительно перепроверить VIP
- Установка
-
- Скомпилировать BhopVip.cs в .dll (если хотите)
- Поместить в addons/counterstrikesharp/plugins/BhopVip/ все файлы скомпилированные тут bin/Release/net8.0/
- Запустить сервер — создастся configs/plugins/BhopVip/config.json
- Отредактировать конфиг (указать данные БД)
- Перезагрузить сервер.