MapChooser простой плагин для CS2 на CounterStrikeSharp, добавляющий команду !rtv для голосования за смену карты. Поддерживает обычные карты и карты из Steam Workshop.
Путь хранения плагина(Установка): Скопируйте плагин в указанный путь и перезапустите сервер. : addons/counterstrikesharp/plugins/MapChooser
При первом запуске автоматически создаётся настраиваемый файл settings.json со списком карт и параметрами плагина.
Исходный код:
Путь хранения плагина(Установка): Скопируйте плагин в указанный путь и перезапустите сервер. : addons/counterstrikesharp/plugins/MapChooser
При первом запуске автоматически создаётся настраиваемый файл settings.json со списком карт и параметрами плагина.
Исходный код:
C-подобный:
using CounterStrikeSharp.API;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Attributes;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Cvars;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Timers;
using CounterStrikeSharp.API.Modules.Utils;
using System.Text.Json;
namespace MapChooser;
[MinimumApiVersion(200)]
public class MapChooser : BasePlugin
{
public override string ModuleName => "MapChooser";
public override string ModuleVersion => "1.0.0";
public override string ModuleAuthor => "Alley";
private Config _config = null!;
private readonly Dictionary<int, UserSettings> _users = new();
private ChatMenu? _voteMenu;
private readonly Dictionary<string, int> _mapVotes = new();
private readonly HashSet<string> _nominatedMaps = new();
private readonly HashSet<string> _recentMaps = new();
private int _rtvCount = 0;
private bool _voteInProgress = false;
private bool _rtvAllowedByTime = true;
private string? _nextMap = null;
private float _mapStartTime = 0f;
public override void Load(bool hotReload)
{
_config = LoadConfig();
RegisterListener<Listeners.OnMapStart>(name =>
{
_mapStartTime = Server.CurrentTime;
_rtvCount = 0;
_voteInProgress = false;
_rtvAllowedByTime = true;
_nextMap = null;
_mapVotes.Clear();
_nominatedMaps.Clear();
_users.Clear();
var currentMapKey = _config.Maps.FirstOrDefault(m => m.MapId.Equals(name, StringComparison.OrdinalIgnoreCase))?.MapId ?? name;
_recentMaps.Add(currentMapKey);
if (_recentMaps.Count > _config.RecentMapsCount)
{
_recentMaps.Remove(_recentMaps.First());
}
if (_config.RtvDelayMinutes > 0)
{
_rtvAllowedByTime = false;
AddTimer(_config.RtvDelayMinutes * 60f, () => _rtvAllowedByTime = true);
}
var timeLimit = ConVar.Find("mp_timelimit")?.GetPrimitiveValue<float>() ?? 0f;
if (timeLimit > 0)
{
var delay = (timeLimit - _config.VoteStartMinutes) * 60f;
if (delay > 0)
{
AddTimer(delay, () => StartMapVote());
}
}
});
AddCommand("rtv", "Rock the vote", OnRtvCommand);
AddCommand("rockthevote", "Rock the vote", OnRtvCommand);
AddCommand("nominate", "Nominate a map", OnNominateCommand);
AddCommand("timeleft", "Show time left", OnTimeleftCommand);
AddCommand("nextmap", "Show next map", OnNextmapCommand);
AddCommand("maplist", "Show available maps", OnMapListCommand);
}
private void OnMapListCommand(CCSPlayerController? player, CommandInfo info)
{
if (player == null) return;
var available = GetAvailableMaps();
if (available.Count == 0)
{
player.PrintToChat($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} Нет доступных карт.");
return;
}
player.PrintToChat($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
player.PrintToChat($"{ChatColors.Gold} ДОСТУПНЫЕ КАРТЫ");
player.PrintToChat($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
int index = 1;
foreach (var map in available)
{
var status = _nominatedMaps.Contains(map.MapId) ? $"{ChatColors.LightPurple} (номинирована)" : "";
player.PrintToChat($" {ChatColors.Grey}{index++}. {ChatColors.Green}{map.DisplayName} {ChatColors.Grey}→ {ChatColors.DarkBlue}{map.MapId}{status}");
}
player.PrintToChat($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
}
private void OnRtvCommand(CCSPlayerController? player, CommandInfo info)
{
if (player == null || !player.IsValid || player.IsBot) return;
var alivePlayers = Utilities.GetPlayers().Count(p => p is { IsValid: true, IsBot: false });
if (alivePlayers < _config.RtvMinPlayers && !_config.RtvAllowWhenLessThanMin)
{
player.PrintToChat($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} RTV доступен с {_config.RtvMinPlayers} игроков! (сейчас: {alivePlayers})");
return;
}
if (!_rtvAllowedByTime)
{
player.PrintToChat($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} RTV доступен через {_config.RtvDelayMinutes} мин после начала карты.");
return;
}
if (_voteInProgress)
{
player.PrintToChat($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} Голосование уже идёт!");
return;
}
if (_users.ContainsKey(player.Slot))
{
player.PrintToChat($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} Вы уже использовали RTV!");
return;
}
_users[player.Slot] = new UserSettings { HasRtved = true };
_rtvCount++;
var required = Math.Max(1, (int)Math.Ceiling(alivePlayers * _config.RtvRequiredPercent));
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] {ChatColors.LightBlue}{player.PlayerName} {ChatColors.Green}хочет сменить карту! {ChatColors.Gold}({_rtvCount}/{required})");
if (_rtvCount >= required)
{
StartMapVote(true);
}
}
private void OnNominateCommand(CCSPlayerController? player, CommandInfo info)
{
if (player == null || !player.IsValid || player.IsBot) return;
var available = GetAvailableMaps();
if (available.Count == 0)
{
player.PrintToChat($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} Нет карт для номинации.");
return;
}
var menu = new ChatMenu("Номинировать карту");
foreach (var map in available)
{
menu.AddMenuOption(map.DisplayName, (p, option) =>
{
if (_nominatedMaps.Contains(map.MapId)) return;
_nominatedMaps.Add(map.MapId);
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] {ChatColors.LightBlue}{p.PlayerName} {ChatColors.Green}номинировал: {ChatColors.Gold}{map.DisplayName}");
});
}
MenuManager.OpenChatMenu(player, menu);
}
private void OnTimeleftCommand(CCSPlayerController? player, CommandInfo info)
{
if (player == null) return;
var timeLimit = ConVar.Find("mp_timelimit")?.GetPrimitiveValue<float>() ?? 0f;
if (timeLimit > 0)
{
var remaining = timeLimit * 60f - (Server.CurrentTime - _mapStartTime);
if (remaining < 0) remaining = 0;
var mins = (int)remaining / 60;
var secs = (int)remaining % 60;
player.PrintToChat($"{ChatColors.Green}[MapChooser] Осталось: {ChatColors.Gold}{mins:D2}:{secs:D2}");
}
else
{
player.PrintToChat($"{ChatColors.Green}[MapChooser] Без лимита времени.");
}
}
private void OnNextmapCommand(CCSPlayerController? player, CommandInfo info)
{
if (player == null) return;
if (string.IsNullOrEmpty(_nextMap))
player.PrintToChat($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} Карта не выбрана.");
else
{
var map = _config.Maps.FirstOrDefault(m => m.MapId == _nextMap);
var name = map?.DisplayName ?? _nextMap;
player.PrintToChat($"{ChatColors.Green}[MapChooser] Следующая: {ChatColors.Gold}{name}");
}
}
private void StartMapVote(bool forced = false)
{
if (_voteInProgress) return;
_voteInProgress = true;
var maps = GetVoteMaps();
Server.PrintToChatAll($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Server.PrintToChatAll($"{ChatColors.Gold} ГОЛОСОВАНИЕ ЗА КАРТУ");
Server.PrintToChatAll($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
_voteMenu = new ChatMenu("Голосуйте за карту");
_voteMenu.AddMenuOption("Extend current map", (p, option) => VoteForMap("extend", p));
int index = 1;
foreach (var map in maps)
{
_voteMenu.AddMenuOption(map.DisplayName, (p, option) => VoteForMap(map.MapId, p));
Server.PrintToChatAll($" {ChatColors.Grey}{index++}. {ChatColors.Green}{map.DisplayName}");
}
Server.PrintToChatAll($" {ChatColors.Grey}0. {ChatColors.LightPurple}Extend current map");
Server.PrintToChatAll($"{ChatColors.Gold}Голосуйте в меню! Время: {_config.VoteTime} сек");
Server.PrintToChatAll($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
foreach (var player in Utilities.GetPlayers().Where(p => p.IsValid && !p.IsBot))
{
MenuManager.OpenChatMenu(player, _voteMenu);
}
AddTimer(_config.VoteTime, EndVote);
}
private void VoteForMap(string mapId, CCSPlayerController player)
{
if (!_mapVotes.ContainsKey(mapId))
_mapVotes[mapId] = 0;
_mapVotes[mapId]++;
var map = _config.Maps.FirstOrDefault(m => m.MapId == mapId);
var name = mapId == "extend" ? "Extend current map" : (map?.DisplayName ?? mapId);
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] {ChatColors.LightBlue}{player.PlayerName} {ChatColors.Green}voted for {ChatColors.Gold}{name}");
}
private void EndVote()
{
Server.PrintToChatAll($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Server.PrintToChatAll($"{ChatColors.Gold} VOTING ENDED!");
Server.PrintToChatAll($"{ChatColors.LightBlue}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
var winner = _mapVotes.OrderByDescending(kv => kv.Value).FirstOrDefault();
if (winner.Key == "extend")
{
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] Map extended for {_config.ExtendMinutes} minutes!");
var current = ConVar.Find("mp_timelimit")!.GetPrimitiveValue<float>();
ConVar.Find("mp_timelimit")?.SetValue(current + _config.ExtendMinutes);
}
else if (!string.IsNullOrEmpty(winner.Key))
{
var map = _config.Maps.First(m => m.MapId == winner.Key);
_nextMap = winner.Key;
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] Winner: {ChatColors.Gold}{map.DisplayName} {ChatColors.Green}(votes: {winner.Value})!");
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] Map change in 5 seconds...");
AddTimer(5f, () => ChangeToMap(winner.Key));
}
else
{
var randomMap = GetAvailableMaps().OrderBy(_ => Random.Shared.Next()).FirstOrDefault();
if (randomMap != null)
{
_nextMap = randomMap.MapId;
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] Randomly selected: {ChatColors.Gold}{randomMap.DisplayName}");
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] Map change in 5 seconds...");
AddTimer(5f, () => ChangeToMap(randomMap.MapId));
}
}
_voteInProgress = false;
}
private List<MapEntry> GetVoteMaps()
{
var available = GetAvailableMaps();
var selected = _nominatedMaps
.Where(id => available.Any(m => m.MapId == id))
.Select(id => available.First(m => m.MapId == id))
.ToList();
while (selected.Count < 6 && available.Count > selected.Count)
{
var randomMap = available.Except(selected).OrderBy(_ => Random.Shared.Next()).FirstOrDefault();
if (randomMap != null) selected.Add(randomMap);
}
return selected;
}
private List<MapEntry> GetAvailableMaps()
{
return _config.Maps.Where(m => !_recentMaps.Contains(m.MapId)).ToList();
}
private void ChangeToMap(string mapId)
{
var map = _config.Maps.FirstOrDefault(m => m.MapId == mapId);
if (map == null)
{
Server.PrintToChatAll($"{ChatColors.Red}{ChatColors.Green}[MapChooser]{ChatColors.Red} Error: map not found!");
return;
}
Server.PrintToChatAll($"{ChatColors.Green}[MapChooser] Loading map: {ChatColors.Gold}{map.DisplayName}");
if (map.MapId.StartsWith("workshop_id/"))
{
var workshopId = map.MapId.Replace("workshop_id/", "").Trim();
Server.ExecuteCommand($"host_workshop_map {workshopId}");
}
else if (map.MapId.StartsWith("collection/"))
{
var collectionId = map.MapId.Replace("collection/", "");
Server.ExecuteCommand($"host_workshop_collection {collectionId}");
Server.ExecuteCommand("map random");
}
else if (map.MapId.StartsWith("workshop_start_map/"))
{
var startMapId = map.MapId.Replace("workshop_start_map/", "");
Server.ExecuteCommand($"workshop_start_map {startMapId}");
}
else if (map.MapId.StartsWith("workshop/"))
{
var parts = map.MapId.Split('/', 3);
if (parts.Length == 3)
{
var mapName = parts[2];
Server.ExecuteCommand($"ds_workshop_changelevel {mapName}");
}
}
else
{
Server.ExecuteCommand($"changelevel {map.MapId}");
}
}
private Config LoadConfig()
{
var path = Path.Combine(ModuleDirectory, "settings.json");
if (!File.Exists(path))
{
var defaultConfig = new Config
{
Maps = new List<MapEntry>
{
new() { DisplayName = "Mirage FPS", MapId = "workshop_id/3349182536" },
new() { DisplayName = "Mirage", MapId = "de_mirage" },
new() { DisplayName = "Italy", MapId = "cs_italy" },
new() { DisplayName = "Office", MapId = "cs_office" },
new() { DisplayName = "Inferno", MapId = "de_inferno" }
},
RtvMinPlayers = 4,
RtvAllowWhenLessThanMin = false,
RtvRequiredPercent = 0.6,
RtvDelayMinutes = 5,
VoteTime = 20f,
RecentMapsCount = 5,
VoteStartMinutes = 5f,
ExtendMinutes = 15f
};
var jsonOptions = new JsonSerializerOptions { WriteIndented = true };
File.WriteAllText(path, JsonSerializer.Serialize(defaultConfig, jsonOptions));
return defaultConfig;
}
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<Config>(json)!;
}
}
public class UserSettings
{
public bool HasRtved { get; set; }
}
public class MapEntry
{
public string DisplayName { get; set; } = "";
public string MapId { get; set; } = "";
}
public class Config
{
public List<MapEntry> Maps { get; set; } = new();
public int RtvMinPlayers { get; set; } = 4;
public bool RtvAllowWhenLessThanMin { get; set; } = false;
public double RtvRequiredPercent { get; set; } = 0.6;
public int RtvDelayMinutes { get; set; } = 5;
public float VoteTime { get; set; } = 20f;
public int RecentMapsCount { get; set; } = 5;
public float VoteStartMinutes { get; set; } = 5f;
public float ExtendMinutes { get; set; } = 15f;
}
- Требования
-
CounterStrikeSharp.API
- Переменные
-
settings.json список карт и параметры плагина.
- Команды
-
!rtv — запуск голосования за смену карты
- Установка
-
Путь хранения плагина(Установка): Скопируйте плагин в указанный путь и перезапустите сервер. : addons/counterstrikesharp/plugins/MapChooser