From 29805359205e37070c071d4b5c658fc9ca2f7f33 Mon Sep 17 00:00:00 2001 From: Abner Santos Date: Sat, 13 Jan 2024 20:59:28 -0300 Subject: [PATCH] Use chatmenu for votes, add nominate (#3) * Use chatmenu and add nominate, huge rewrite * update readme --- AsyncVoteManager.cs | 51 +++++++++++ AsyncVoteValidator.cs | 22 +++++ Config.cs | 15 +--- NominationManager.cs | 25 ++++++ README.md | 25 ++++-- RockTheVote.cs | 195 ++++++++++++++++++++++++++++++------------ RockTheVote.csproj | 6 ++ RtvManager.cs | 72 ---------------- ServerManager.cs | 14 +++ Translations.cs | 99 --------------------- VoteManager.cs | 123 ++++++++++++++++++++++++++ maplist.txt | 11 +++ 12 files changed, 415 insertions(+), 243 deletions(-) create mode 100644 AsyncVoteManager.cs create mode 100644 AsyncVoteValidator.cs create mode 100644 NominationManager.cs delete mode 100644 RtvManager.cs create mode 100644 ServerManager.cs delete mode 100644 Translations.cs create mode 100644 VoteManager.cs create mode 100644 maplist.txt diff --git a/AsyncVoteManager.cs b/AsyncVoteManager.cs new file mode 100644 index 0000000..2e71c8a --- /dev/null +++ b/AsyncVoteManager.cs @@ -0,0 +1,51 @@ + +namespace cs2_rockthevote +{ + public enum VoteResult { + Added, + AlreadyAddedBefore, + VotesAlreadyReached, + VotesReached + } + + public class AsyncVoteManager + { + private List votes = new(); + public int VoteCount => votes.Count; + public int RequiredVotes => VoteValidator.RequiredVotes; + + public AsyncVoteManager(AsyncVoteValidator voteValidator) + { + VoteValidator = voteValidator; + } + + private readonly AsyncVoteValidator VoteValidator; + + public bool VotesAlreadyReached { get; set; } = false; + + public VoteResult AddVote(int userId) + { + if (VotesAlreadyReached) + return VoteResult.VotesAlreadyReached; + + if (votes.IndexOf(userId) != -1) + return VoteResult.AlreadyAddedBefore; + + votes.Add(userId); + if(VoteValidator.CheckVotes(votes.Count)) + { + VotesAlreadyReached = true; + return VoteResult.VotesReached; + } + + return VoteResult.Added; + } + + public void RemoveVote(int userId) + { + var index = votes.IndexOf(userId); + if(index > -1) + votes.RemoveAt(index); + } + } +} diff --git a/AsyncVoteValidator.cs b/AsyncVoteValidator.cs new file mode 100644 index 0000000..b0ae0ff --- /dev/null +++ b/AsyncVoteValidator.cs @@ -0,0 +1,22 @@ +namespace cs2_rockthevote +{ + public class AsyncVoteValidator + { + private float VotePercentage = 0F; + + private readonly ServerManager Server; + + public int RequiredVotes { get => (int)Math.Ceiling(Server.ValidPlayerCount * VotePercentage); } + + public AsyncVoteValidator(int votePercentage, ServerManager server) + { + VotePercentage = votePercentage / 100F; + Server = server; + } + + public bool CheckVotes(int numberOfVotes) + { + return numberOfVotes >= RequiredVotes; + } + } +} diff --git a/Config.cs b/Config.cs index 296c077..b632b99 100644 --- a/Config.cs +++ b/Config.cs @@ -1,19 +1,12 @@ using CounterStrikeSharp.API.Core; -using System.Text.Json.Serialization; namespace cs2_rockthevote { public class Config : IBasePluginConfig { - [JsonPropertyName("MinPlayers")] - public int MinPlayers { get; set; } = 0; - - [JsonPropertyName("VotePercentage")] - public decimal VotePercentage { get; set; } = 0.6M; - - [JsonPropertyName("Language")] - public string Language { get; set; } = "en"; - - public int Version { get; set; } = 1; + public int Version { get; set; } = 2; + public int RtvVotePercentage { get; set; } = 60; + public int RtvMinPlayers { get; set; } = 0; + public bool DisableVotesInWarmup { get; set; } = false; } } diff --git a/NominationManager.cs b/NominationManager.cs new file mode 100644 index 0000000..25272ad --- /dev/null +++ b/NominationManager.cs @@ -0,0 +1,25 @@ + +namespace cs2_rockthevote +{ + public class NominationManager + { + Dictionary Nominations = new(); + + public void Nominate(int userId, string map) + { + Nominations[userId] = map; + } + + public List Votes() + { + return Nominations + .Select(x => x.Value) + .Distinct() + .Select(map => new KeyValuePair(map, Nominations.Select(x => x.Value == map).Count())) + .OrderByDescending(x => x.Value) + .Select(x => x.Key) + .Take(5) + .ToList(); + } + } +} diff --git a/README.md b/README.md index fd4ac88..b56c439 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # CS2 Rock The Vote -Players can type rtv to request the map to be changed, once a number of votes is reached (by default 60% of players in the server) the map will end in the end of the round and trigger a next map vote (that you need to configure yourself using CS2 built in nextmap vote system) +Players can type rtv to request the map to be changed, once a number of votes is reached (by default 60% of players in the server) a vote will start for the next map, this vote lasts up to 30 seconds (hardcoded for now), in the end server changes to the winner map. + +# Features +- Reads from a custom maplist +- nominate command # Limitations - I haven't tested this with a server with more than 1 player, so this is a WIP version, I will address all feedback and issues that appear. - - This is intended to be used alongside the built in map vote system in CS2 so you need to configure end of map vote in CS2 yourself. - - For now only English and Brazilian Portuguese are supported languages, adding translations require recompiling the plugin for now, this will likely change in the near future, feel fre to open a PR adding a new language, I will be glad to review and recompile the plugin myself. + - Previous version relied on the official CS2 vote system, I pivoted this idea in favor of adding nominate, I will probably create another plugin with the original idea as soon as I figure out how to do the nominate command that way. + # Requirements [Latest release of Counter Strike Sharp](https://github.com/roflmuffin/CounterStrikeSharp) @@ -20,9 +24,16 @@ Players can type rtv to request the map to be changed, once a number of votes is ```json { - "MinPlayers": 0, // Number of players required to enable the command - "VotePercentage": 0.6, // Percentage of votes required to change the map - "Language": "en", // The language, for now only en and pt are valid values - "Version": 1 // Don't chang this + "Version": 2, + "RtvVotePercentage": 60, + "RtvMinPlayers": 0, + "DisableVotesInWarmup": false } ``` + +Maps that will be used in RTV are located in addons/counterstrikesharp/configs/plugins/RockTheVote/maplist.txt + +# TODO +- Add minimum rounds to use commands. +- Add votemap. +- Translations support diff --git a/RockTheVote.cs b/RockTheVote.cs index 9237064..c9d0c31 100644 --- a/RockTheVote.cs +++ b/RockTheVote.cs @@ -1,32 +1,28 @@ using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Core.Attributes.Registration; +using CounterStrikeSharp.API.Modules.Commands; +using System.ComponentModel.Design; using static CounterStrikeSharp.API.Core.Listeners; namespace cs2_rockthevote { public class RockTheVote : BasePlugin, IPluginConfig { - public override string ModuleName => "AbNeR Rock The Vote"; - public override string ModuleVersion => "0.0.1"; - public override string ModuleAuthor => "AbNeR_CSS"; + public override string ModuleName => "RockTheVote"; + public override string ModuleVersion => "0.0.2"; + public override string ModuleAuthor => "abnerfs"; public override string ModuleDescription => "You know what it is, rtv"; CCSGameRules? _gameRules = null; + ServerManager ServerManager = new(); + NominationManager NominationManager = new(); + AsyncVoteManager Rtv = null; + List Maps = new(); - RtvManager? _rtvManager; - Translations? _translations { get; set; } public Config? Config { get; set; } - public void OnConfigParsed(Config config) - { - Console.WriteLine("RockTheVote Config parsed"); - Config = config; - _translations = new Translations(config.Language); - SetupRtvManager(); - } - public bool WarmupRunning { get @@ -38,79 +34,170 @@ public bool WarmupRunning } } - void NewVoteCallback(object? _e, NewVoteArgs args) + void LoadMaps() { - var player = Utilities.GetPlayerFromUserid(args.UserId); - if (player.IsValid) - if (args.AlreadyVoted) - Server.PrintToChatAll(_translations!.ParseMessage(Translations.AlreadyVoted, new TranslationParams { Voted = args.Votes, VotesNeeded = args.VotesNeeded })); - else - Server.PrintToChatAll(_translations!.ParseMessage(Translations.Voted, new TranslationParams { PlayerName = player.PlayerName, Voted = args.Votes, VotesNeeded = args.VotesNeeded })); + Maps = new List(); + string mapsFile = Path.Combine(ModuleDirectory, "maplist.txt"); + if (!File.Exists(mapsFile)) + throw new FileNotFoundException(mapsFile); + + Maps = File.ReadAllText(mapsFile) + .Replace("\r\n", "\n") + .Split("\n") + .Select(x => x.Trim()) + .Where(x => !x.StartsWith("//")) + .Where(x => Server.IsMapValid(x)) + .ToList(); } - void VotesReachedCallback(object? _e, EventArgs args) + public override void Load(bool hotReload) { - Server.ExecuteCommand("mp_timelimit 0.01"); - Server.ExecuteCommand("mp_maxrounds 0"); - Server.PrintToChatAll(_translations!.ParseMessage(Translations.VotesReached, new TranslationParams())); + Init(); + RegisterListener((_mapname) => Init()); } - void SetupRtvManager() + void Init() { - _rtvManager = new(Config!); - _rtvManager.NewVoteEvent += NewVoteCallback; - _rtvManager.VotesReachedEvent += VotesReachedCallback; + NominationManager = new(); + LoadMaps(); + _gameRules = null; + AddTimer(1.0F, SetGameRules); + if (Config is not null) + { + AsyncVoteValidator validator = new(Config!.RtvVotePercentage, ServerManager); + Rtv = new AsyncVoteManager(validator); + } } - public override void Load(bool hotReload) + void SetGameRules() => _gameRules = Utilities.FindAllEntitiesByDesignerName("cs_gamerules").First().GameRules!; + + bool ValidateCommand(CCSPlayerController? player) { - OnMapStart(Server.MapName); - RegisterListener(OnMapStart); - } + if (player is null || !player.IsValid) return false; + if (WarmupRunning && Config!.DisableVotesInWarmup) + { + player.PrintToChat("[RockTheVote] Command disabled during warmup."); + return false; + } - void SetGameRules() => _gameRules = Utilities.FindAllEntitiesByDesignerName("cs_gamerules").First().GameRules!; - void OnMapStart(string _mapname) - { - _gameRules = null; - AddTimer(1.0F, SetGameRules); - SetupRtvManager(); - } + if (ServerManager.ValidPlayerCount < Config!.RtvMinPlayers) + { + player.PrintToChat($"[RockTheVote] Minimum players to use this command is {Config.RtvMinPlayers}"); + return false; + } + return true; + } [GameEventHandler(HookMode.Pre)] public HookResult EventPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo @eventInfo) { var userId = @event.Userid.UserId!.Value; - _rtvManager!.RemovePlayer(userId); + Rtv.RemoveVote(userId); return HookResult.Continue; } + void NominateHandler(CCSPlayerController? player, string map) + { + if (string.IsNullOrEmpty(map)) + { + player!.PrintToChat($"[RockTheVote] Usage: nominate "); + } + else if (Server.IsMapValid(map)) + { + if (map == Server.MapName) + { + player!.PrintToChat($"[RockTheVote] You can't nominate the current map"); + return; + } + + NominationManager.Nominate(player.UserId!.Value, map); + Server.PrintToChatAll($"[RockTheVote] Player {player.PlayerName} nominated map {map}"); + } + else + { + player!.PrintToChat($"[RockTheVote] Invalid map"); + } + } + + [ConsoleCommand("nominate", "nominate a map to rtv")] + public void OnNominate(CCSPlayerController? player, CommandInfo command) + { + if (!ValidateCommand(player)) + return; + + string map = command.GetArg(1); + NominateHandler(player, map); + } + + IList Shuffle(Random rng, IList array) + { + int n = array.Count; + while (n > 1) + { + int k = rng.Next(n--); + T temp = array[n]; + array[n] = array[k]; + array[k] = temp; + } + return array; + } + + + [ConsoleCommand("rtv", "Votes to rock the vote")] + public void OnRTV(CCSPlayerController? player, CommandInfo? command) + { + if (!ValidateCommand(player)) + return; + + VoteResult result = Rtv.AddVote(player!.UserId!.Value); + switch (result) + { + case VoteResult.Added: + Server.PrintToChatAll($"[RockTheVote] {player.PlayerName} wants to rock the vote ({Rtv.VoteCount} voted, {Rtv.RequiredVotes} needed)"); + break; + case VoteResult.AlreadyAddedBefore: + player.PrintToChat($"[RockTheVote] You already rocked the vote ({Rtv.VoteCount} voted, {Rtv.RequiredVotes} needed)"); + break; + case VoteResult.VotesReached: + Server.PrintToChatAll("[RockTheVote] Number of votes reached, the vote for the next map will start"); + var mapsScrambled = Shuffle(new Random(), Maps.Where(x => x != Server.MapName).ToList()); + var maps = NominationManager.Votes().Concat(mapsScrambled).Distinct().ToList(); + VoteManager manager = new(maps!, this, 30, ServerManager.ValidPlayerCount); + manager.StartVote(); + break; + } + } [GameEventHandler(HookMode.Post)] public HookResult OnChat(EventPlayerChat @event, GameEventInfo info) { var player = Utilities.GetPlayerFromUserid(@event.Userid); - if (player is null || !player.IsValid || _rtvManager is null || Config is null) + if (!ValidateCommand(player)) return HookResult.Continue; + var text = @event.Text.Trim().ToLower(); if (@event.Text.Trim() == "rtv") { - if (WarmupRunning) - { - player.PrintToChat(_translations!.ParseMessage(Translations.WarmupRunning, new TranslationParams())); - } - else if (_rtvManager.NumberOfPlayers < Config.MinPlayers) - { - player.PrintToChat(_translations!.ParseMessage(Translations.MinimumPlayers, new TranslationParams() { MinPlayers = Config.MinPlayers })); - } - else - { - _rtvManager!.AddPlayer(player.UserId!.Value); - } + OnRTV(player, null); + } + else if (text.StartsWith("nominate")) + { + + var split = text.Split("nominate"); + var map = split.Length > 1 ? split[1].Trim() : ""; + NominateHandler(player, map); } + return HookResult.Continue; } + + public void OnConfigParsed(Config config) + { + Config = config; + Init(); + } } -} \ No newline at end of file +} diff --git a/RockTheVote.csproj b/RockTheVote.csproj index 4bb5dd0..7673c5b 100644 --- a/RockTheVote.csproj +++ b/RockTheVote.csproj @@ -14,4 +14,10 @@ + + + PreserveNewest + + + diff --git a/RtvManager.cs b/RtvManager.cs deleted file mode 100644 index 21233a4..0000000 --- a/RtvManager.cs +++ /dev/null @@ -1,72 +0,0 @@ -using CounterStrikeSharp.API; - -namespace cs2_rockthevote -{ - public class NewVoteArgs - { - public int UserId { get; init; } - public int Votes { get; init; } - public int VotesNeeded { get; init; } - public bool AlreadyVoted { get; init; } - - public NewVoteArgs(int userId, int votes, int requiredVotes, bool alreadyVoted) - { - UserId = userId; - Votes = votes; - VotesNeeded = requiredVotes; - AlreadyVoted = alreadyVoted; - } - } - - public class RtvManager - { - public event EventHandler? NewVoteEvent; - public event EventHandler? VotesReachedEvent; - public int NumberOfPlayers - { - get => Utilities.GetPlayers() - .Where(x => !x.IsBot) - .Count(); - } - - private Config _config; - - public bool VotesReached { get => Votes.Count >= RequiredVotes; } - - public RtvManager(Config config) - { - _config = config; - } - - int RequiredVotes { get => (int)Math.Ceiling(NumberOfPlayers * _config.VotePercentage); } - - List Votes { get; set; } = new(); - - void CheckVotes() - { - if (VotesReached) - VotesReachedEvent?.Invoke(this, new EventArgs()); - } - - public void AddPlayer(int userId) - { - if (VotesReached) - return; - - bool alreadyVoted = Votes.IndexOf(userId) > -1; - if (!alreadyVoted) - Votes.Add(userId); - - NewVoteEvent?.Invoke(this, new NewVoteArgs(userId, Votes.Count, RequiredVotes, alreadyVoted)); - CheckVotes(); - } - - public void RemovePlayer(int userId) - { - var index = Votes.IndexOf(userId); - if (index > -1) - Votes.RemoveAt(index); - CheckVotes(); - } - } -} diff --git a/ServerManager.cs b/ServerManager.cs new file mode 100644 index 0000000..0e3f95a --- /dev/null +++ b/ServerManager.cs @@ -0,0 +1,14 @@ +using CounterStrikeSharp.API; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace cs2_rockthevote +{ + public class ServerManager + { + public int ValidPlayerCount { get => Utilities.GetPlayers().Where(x => x.IsValid && !x.IsBot).Count(); } + } +} diff --git a/Translations.cs b/Translations.cs deleted file mode 100644 index 0325a08..0000000 --- a/Translations.cs +++ /dev/null @@ -1,99 +0,0 @@ -using CounterStrikeSharp.API.Modules.Utils; -using System.Text; - -namespace cs2_rockthevote -{ - public class TranslationParams - { - public string? PlayerName { get; set; } - public int? Voted { get; set; } - public int? VotesNeeded { get; set; } - public int? MinPlayers { get; set; } - } - - public class Translations - { - static Dictionary ColorDictionary = new() - { - {"{DEFAULT}", ChatColors.Default.ToString()}, - {"{WHITE}", ChatColors.White.ToString()}, - {"{DARKRED}", ChatColors.Darkred.ToString()}, - {"{GREEN}", ChatColors.Green.ToString()}, - {"{LIGHTYELLOW}", ChatColors.LightYellow.ToString()}, - {"{LIGHTBLUE}", ChatColors.LightBlue.ToString()}, - {"{OLIVE}", ChatColors.Olive.ToString()}, - {"{LIME}", ChatColors.Lime.ToString()}, - {"{RED}", ChatColors.Red.ToString()}, - {"{PURPLE}", ChatColors.Purple.ToString()}, - {"{GREY}", ChatColors.Grey.ToString()}, - {"{YELLOW}", ChatColors.Yellow.ToString()}, - {"{GOLD}", ChatColors.Gold.ToString()}, - {"{SILVER}", ChatColors.Silver.ToString()}, - {"{BLUE}", ChatColors.Blue.ToString()}, - {"{DARKBLUE}", ChatColors.DarkBlue.ToString()}, - {"{BLUEGREY}", ChatColors.BlueGrey.ToString()}, - {"{MAGENTA}", ChatColors.Magenta.ToString()}, - {"{LIGHTRED}", ChatColors.LightRed.ToString()}, - }; - private string _language; - - public static string Prefix { get; set; } = " {GREEN}[AbNeR RockTheVote]{DEFAULT} "; - - public static Dictionary Voted { get; } = new Dictionary() - { - {"en", "{PLAYER} wants to rock the vote ({VOTES} voted, {VOTES_NEEDED} needed)" }, - { "pt", "{PLAYER} quer trocar de mapa ({VOTES} votos, {VOTES_NEEDED} necessários)"} - }; - - public static Dictionary AlreadyVoted { get; } = new Dictionary() - { - {"en", "You already rocked the vote ({VOTES} voted, {VOTES_NEEDED} needed)" }, - {"pt", "Você já votou para trocar de mapa ({VOTES} votos, {VOTES_NEEDED} necessários)" } - }; - - public static Dictionary MinimumPlayers { get; } = new() - { - {"en", "Minimum players to use rtv is {MINPLAYERS}" }, - { "pt", "O mínimo de jogadores para usar o rtv é {MINPLAYERS}"} - }; - - public static Dictionary VotesReached { get; set; } = new() - { - {"en", "Number of votes reached, this is the last round!" }, - {"pt", "Número de votos atingido, essa é a última rodada!" } - }; - - public static Dictionary WarmupRunning { get; } = new() - { - {"en", "RTV disabled during wamup" }, - {"pt", "RTV desativado durante o aquecimento" } - }; - - static string ReplaceColors(string message) - { - foreach (var kv in ColorDictionary) - message = message.Replace(kv.Key, kv.Value); - - return message; - } - - public Translations(string language) - { - _language = language; - } - - public string ParseMessage(Dictionary message, TranslationParams @params) - { - var messageStr = message.ContainsKey(_language) ? message[_language] : message["en"]; - StringBuilder builder = new(); - builder.Append(Prefix); - builder.Append(messageStr - .Replace("{PLAYER}", @params.PlayerName ?? "") - .Replace("{VOTES}", @params.Voted.ToString() ?? "0") - .Replace("{VOTES_NEEDED}", @params.VotesNeeded.ToString() ?? "0") - .Replace("{MINPLAYERS}", @params.MinPlayers.ToString() ?? "0")); - - return ReplaceColors(builder.ToString()); - } - } -} diff --git a/VoteManager.cs b/VoteManager.cs new file mode 100644 index 0000000..a326c8c --- /dev/null +++ b/VoteManager.cs @@ -0,0 +1,123 @@ +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Menu; +using CounterStrikeSharp.API.Modules.Timers; +using System.Text; + +namespace cs2_rockthevote +{ + public class VoteManager + { + public VoteManager(List maps, RockTheVote plugin, int voteDuration, int canVoteCount) + { + Maps = maps; + Plugin = plugin; + Duration = voteDuration; + CanVoteCount = canVoteCount; + } + + private List Maps { get; } + private RockTheVote Plugin { get; } + private int Duration { get; set; } + public int CanVoteCount { get; } + private CounterStrikeSharp.API.Modules.Timers.Timer? Timer { get; set; } + + Dictionary Votes = new(); + int TimeLeft = 0; + + public void MapVoted(CCSPlayerController player, string mapName) + { + Votes[mapName] += 1; + player.PrintToChat($"[RockTheVote] You voted in {mapName}"); + VoteDisplayTick(TimeLeft); + if (Votes.Select(x => x.Value).Sum() >= CanVoteCount) + { + EndVote(); + } + } + + void KillTimer() + { + if(Timer is not null) + { + Timer!.Kill(); + Timer = null; + } + } + + void PrintCenterTextAll(string text) + { + foreach (var player in Utilities.GetPlayers()) + { + if (player.IsValid) + { + player.PrintToCenter(text); + } + } + } + + void VoteDisplayTick(int time) + { + int index = 1; + StringBuilder stringBuilder = new(); + stringBuilder.AppendLine($"Vote for the next map: {time}s"); + foreach (var kv in Votes.OrderByDescending(x => x.Value).Take(2)) { + if(kv.Value > 0) + stringBuilder.AppendLine($"{index++} {kv.Key} ({kv.Value})"); + } + + PrintCenterTextAll(stringBuilder.ToString()); + } + + void EndVote() + { + KillTimer(); + var winner = Votes.OrderByDescending(x => x.Value).First(); + var totalVotes = Votes.Select(x => x.Value).Sum(); + var percent = totalVotes > 0 ? (winner.Value / totalVotes) * 100 : 0; + if(percent > 0) + Server.PrintToChatAll($"[RockTheVote] Vote ended, the next map will be {winner.Key} ({percent}% of {totalVotes} vote(s))"); + else + { + var rnd = new Random(); + winner = Votes.ElementAt(rnd.Next(0, Votes.Count)); + Server.PrintToChatAll($"[RockTheVote] No votes, the next map will be {winner.Key}"); + } + PrintCenterTextAll($"Vote finished, next map: {winner.Key}"); + + Plugin.AddTimer(4.0F, () => + { + Server.ExecuteCommand($"changelevel {winner.Key}"); + }); + } + + public void StartVote() + { + ChatMenu menu = new("Vote for the next map:"); + foreach(var map in Maps.Take(5)) + { + Votes[map] = 0; + menu.AddMenuOption(map, (player, option) => MapVoted(player, map)); + } + + foreach (var player in Utilities.GetPlayers()) + { + if (player.IsValid) + { + ChatMenus.OpenMenu(player, menu); + } + } + TimeLeft = Duration; + Timer = Plugin.AddTimer(1.0F, () => + { + if (TimeLeft <= 0) + { + EndVote(); + + } + else + VoteDisplayTick(TimeLeft--); + }, TimerFlags.REPEAT); + } + } +} diff --git a/maplist.txt b/maplist.txt new file mode 100644 index 0000000..c471487 --- /dev/null +++ b/maplist.txt @@ -0,0 +1,11 @@ +//Put your map list here +de_vertigo +cs_italy +de_inferno +cs_office +de_mirage +de_ancient +de_nuke +de_anubis +de_overpass +de_dust2 \ No newline at end of file