From 72e0a644ab3d6240164a6dd8d6b854a8dc2e4c70 Mon Sep 17 00:00:00 2001 From: Abner Santos Date: Sat, 20 Jan 2024 18:12:11 -0300 Subject: [PATCH] 0.0.6 (#7) * Add nomination menu * Add min rounds to enable command config * Add change immediatly config * Add localization * Add pt-br translation * Update README * Update module version --- AsyncVoteManager.cs | 13 +++++-- AsyncVoteValidator.cs | 2 +- Config.cs | 4 +- NominationManager.cs | 75 ++++++++++++++++++++++++++++++++---- README.md | 20 ++++++---- RockTheVote.cs | 88 ++++++++++++++++++++++++++++++------------- RockTheVote.csproj | 10 +++++ VoteManager.cs | 53 ++++++++++++++++++++------ lang/en.json | 21 +++++++++++ lang/pt-BR.json | 21 +++++++++++ 10 files changed, 249 insertions(+), 58 deletions(-) create mode 100644 lang/en.json create mode 100644 lang/pt-BR.json diff --git a/AsyncVoteManager.cs b/AsyncVoteManager.cs index 2e71c8a..82daa23 100644 --- a/AsyncVoteManager.cs +++ b/AsyncVoteManager.cs @@ -1,4 +1,6 @@  +using CounterStrikeSharp.API.Core; + namespace cs2_rockthevote { public enum VoteResult { @@ -28,17 +30,22 @@ public VoteResult AddVote(int userId) if (VotesAlreadyReached) return VoteResult.VotesAlreadyReached; + VoteResult? result = null; if (votes.IndexOf(userId) != -1) - return VoteResult.AlreadyAddedBefore; + result = VoteResult.AlreadyAddedBefore; + else + { + votes.Add(userId); + result = VoteResult.Added; + } - votes.Add(userId); if(VoteValidator.CheckVotes(votes.Count)) { VotesAlreadyReached = true; return VoteResult.VotesReached; } - return VoteResult.Added; + return result.Value; } public void RemoveVote(int userId) diff --git a/AsyncVoteValidator.cs b/AsyncVoteValidator.cs index b0ae0ff..d4f9f4a 100644 --- a/AsyncVoteValidator.cs +++ b/AsyncVoteValidator.cs @@ -16,7 +16,7 @@ public AsyncVoteValidator(int votePercentage, ServerManager server) public bool CheckVotes(int numberOfVotes) { - return numberOfVotes >= RequiredVotes; + return numberOfVotes > 0 && numberOfVotes >= RequiredVotes; } } } diff --git a/Config.cs b/Config.cs index e52e7a5..24ed9ab 100644 --- a/Config.cs +++ b/Config.cs @@ -4,10 +4,12 @@ namespace cs2_rockthevote { public class Config : IBasePluginConfig { - public int Version { get; set; } = 3; + public int Version { get; set; } = 4; public int RtvVotePercentage { get; set; } = 60; public int RtvMinPlayers { get; set; } = 0; public bool DisableVotesInWarmup { get; set; } = false; public int MapsToShowInVote { get; set; } = 5; + public int MinRounds { get; set; } = 0; + public bool ChangeImmediatly { get; set; } = true; } } diff --git a/NominationManager.cs b/NominationManager.cs index 25272ad..644d0cf 100644 --- a/NominationManager.cs +++ b/NominationManager.cs @@ -1,25 +1,86 @@  +using CounterStrikeSharp.API; +using CounterStrikeSharp.API.Core; +using CounterStrikeSharp.API.Modules.Menu; + namespace cs2_rockthevote { public class NominationManager { - Dictionary Nominations = new(); + Dictionary> Nominations = new(); + ChatMenu? nominationMenu = null; + + public RockTheVote Plugin { get; } + + private string[] Maps; + public NominationManager(RockTheVote plugin, string[] maps) + { + Plugin = plugin; + Maps = maps; + nominationMenu = new("Nomination"); + foreach (var map in Maps) + { + nominationMenu.AddMenuOption(map, (CCSPlayerController player, ChatMenuOption option) => + { + Nominate(player, option.Text); + }); + } + } - public void Nominate(int userId, string map) + public void OpenNominationMenu(CCSPlayerController player) { - Nominations[userId] = map; + ChatMenus.OpenMenu(player!, nominationMenu!); + } + + public void Nominate(CCSPlayerController player, string map) + { + if (Maps.FirstOrDefault(x => x.ToLower() == map) is null) + { + player!.PrintToChat(Plugin.Localize("invalid-map")); + return; + + } + + if (map == Server.MapName) + { + player!.PrintToChat(Plugin.Localize("nominate-current")); + return; + } + + var userId = player.UserId!.Value; + if(!Nominations.ContainsKey(userId)) + Nominations[userId] = new(); + + if(Nominations[userId].IndexOf(map) == -1) + Nominations[userId].Add(map); + + var totalVotes = Nominations.Select(x => x.Value.Where(y => y == map).Count()) + .Sum(); + + Server.PrintToChatAll(Plugin.Localize("nominated", player.PlayerName, map, totalVotes)); } - public List Votes() + public List NominationWinners() { - return Nominations + if(Nominations.Count == 0) + return new List(); + + var rawNominations = Nominations .Select(x => x.Value) + .Aggregate((acc, x) => acc.Concat(x).ToList()); + + return rawNominations .Distinct() - .Select(map => new KeyValuePair(map, Nominations.Select(x => x.Value == map).Count())) + .Select(map => new KeyValuePair(map, rawNominations.Count(x => x == map))) .OrderByDescending(x => x.Value) .Select(x => x.Key) - .Take(5) .ToList(); } + + public void RemoveNominations(int userId) + { + if (!Nominations.ContainsKey(userId)) + Nominations.Remove(userId); + } } } diff --git a/README.md b/README.md index f0ee6ec..8b0a587 100644 --- a/README.md +++ b/README.md @@ -24,19 +24,25 @@ Players can type rtv to request the map to be changed, once a number of votes is ```json { - "Version": 2, + "Version": 4, "RtvVotePercentage": 60, "RtvMinPlayers": 0, "DisableVotesInWarmup": false, - "MapsToShowInVote": 5 + "MapsToShowInVote": 5, + "ChangeImmediatly": true, + "MinRounds": 0 } ``` 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 -- Add dont change option -- Nomination menu +- [X] ~~Add minimum rounds to use commands.~~ +- [ ] Add votemap. +- [x] ~~Translations support~~ +- [ ] Add dont change option +- [x] ~~Nomination menu~~ +- [ ] Add end of map vote +- [ ] Add timeleft command +- [ ] Add currentmap command +- [ ] Add nextmap command diff --git a/RockTheVote.cs b/RockTheVote.cs index 6f94be5..88e6697 100644 --- a/RockTheVote.cs +++ b/RockTheVote.cs @@ -9,18 +9,23 @@ namespace cs2_rockthevote public class RockTheVote : BasePlugin, IPluginConfig { public override string ModuleName => "RockTheVote"; - public override string ModuleVersion => "0.0.5"; + public override string ModuleVersion => "0.0.6"; 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; + NominationManager? NominationManager = null; + AsyncVoteManager? Rtv = null; List Maps = new(); + VoteManager? voteManager = null; public Config? Config { get; set; } + public string Localize(string key, params object[] values) + { + return $"{Localizer["prefix"]}{Localizer[key, values]}"; + } public bool WarmupRunning { @@ -33,6 +38,17 @@ public bool WarmupRunning } } + public int RoundsPlayed + { + get + { + if (_gameRules is null) + SetGameRules(); + + return _gameRules?.TotalRoundsPlayed ?? 0; + } + } + void LoadMaps() { Maps = new List(); @@ -40,12 +56,15 @@ void LoadMaps() 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 => !string.IsNullOrWhiteSpace(x) && !x.StartsWith("//")) .ToList(); + + NominationManager = new(this, Maps.ToArray()); } public override void Load(bool hotReload) @@ -56,7 +75,6 @@ public override void Load(bool hotReload) void Init() { - NominationManager = new(); LoadMaps(); _gameRules = null; AddTimer(1.0F, SetGameRules); @@ -73,15 +91,21 @@ bool ValidateCommand(CCSPlayerController? player) { if (player is null || !player.IsValid) return false; + if(Config!.MinRounds > RoundsPlayed) + { + player!.PrintToChat(Localize("minimum-rounds", Config!.MinRounds)); + return false; + } + if (WarmupRunning && Config!.DisableVotesInWarmup) { - player.PrintToChat("[RockTheVote] Command disabled during warmup."); + player.PrintToChat(Localize("disabled-warmup")); return false; } if (ServerManager.ValidPlayerCount < Config!.RtvMinPlayers) { - player.PrintToChat($"[RockTheVote] Minimum players to use this command is {Config.RtvMinPlayers}"); + player.PrintToChat(Localize("minimum-players", Config.RtvMinPlayers)); return false; } @@ -92,30 +116,24 @@ bool ValidateCommand(CCSPlayerController? player) public HookResult EventPlayerDisconnect(EventPlayerDisconnect @event, GameEventInfo @eventInfo) { var userId = @event.Userid.UserId!.Value; - Rtv.RemoveVote(userId); + Rtv!.RemoveVote(userId); + NominationManager!.RemoveNominations(userId); return HookResult.Continue; } void NominateHandler(CCSPlayerController? player, string map) { - if (string.IsNullOrEmpty(map)) + if(Rtv!.VotesAlreadyReached) { - player!.PrintToChat($"[RockTheVote] Usage: nominate "); + player!.PrintToChat(Localize("nomination-votes-reached")); } - else if (Maps.FirstOrDefault(x => x.ToLower() == map) is not null) + else if (string.IsNullOrEmpty(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}"); + NominationManager!.OpenNominationMenu(player!); } else { - player!.PrintToChat($"[RockTheVote] Invalid map"); + NominationManager!.Nominate(player, map); } } @@ -149,26 +167,42 @@ public void OnRTV(CCSPlayerController? player, CommandInfo? command) if (!ValidateCommand(player)) return; - VoteResult result = Rtv.AddVote(player!.UserId!.Value); + 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)"); + Server.PrintToChatAll(Localize("rocked-the-vote", player.PlayerName, Rtv.VoteCount, Rtv.RequiredVotes)); break; case VoteResult.AlreadyAddedBefore: - player.PrintToChat($"[RockTheVote] You already rocked the vote ({Rtv.VoteCount} voted, {Rtv.RequiredVotes} needed)"); + Server.PrintToChatAll(Localize("already-rocked-the-vote", Rtv.VoteCount, Rtv.RequiredVotes)); break; case VoteResult.VotesReached: - Server.PrintToChatAll("[RockTheVote] Number of votes reached, the vote for the next map will start"); + Server.PrintToChatAll(Localize("starting-vote",player.PlayerName, Rtv.VoteCount, Rtv.RequiredVotes)); var mapsScrambled = Shuffle(new Random(), Maps.Where(x => x != Server.MapName).ToList()); - var maps = NominationManager.Votes().Concat(mapsScrambled).Distinct().ToList(); + var maps = NominationManager!.NominationWinners().Concat(mapsScrambled).Distinct().ToList(); var mapsToShow = Config!.MapsToShowInVote == 0 ? 5 : Config!.MapsToShowInVote; - VoteManager manager = new(maps!, this, 30, ServerManager.ValidPlayerCount, mapsToShow); - manager.StartVote(); + voteManager = new(maps!, this, 30, ServerManager.ValidPlayerCount, mapsToShow, Config.ChangeImmediatly); + voteManager.StartVote(); break; } } + [GameEventHandler(HookMode.Post)] + public HookResult OnRoundEnd(EventRoundEnd @event, GameEventInfo info) + { + if(voteManager?.ChangeNextMap() ?? false) + voteManager = null; + return HookResult.Continue; + } + + [GameEventHandler(HookMode.Post)] + public HookResult OnRoundStart(EventRoundStart @event, GameEventInfo info) + { + if (voteManager?.ChangeNextMap() ?? false) + voteManager = null; + return HookResult.Continue; + } + [GameEventHandler(HookMode.Post)] public HookResult OnChat(EventPlayerChat @event, GameEventInfo info) { diff --git a/RockTheVote.csproj b/RockTheVote.csproj index 7673c5b..1de1952 100644 --- a/RockTheVote.csproj +++ b/RockTheVote.csproj @@ -7,6 +7,10 @@ enable + + + + ..\..\..\..\..\CSSHARP\CounterStrikeSharp.API.dll @@ -15,6 +19,12 @@ + + Always + + + PreserveNewest + PreserveNewest diff --git a/VoteManager.cs b/VoteManager.cs index 847d781..3a87820 100644 --- a/VoteManager.cs +++ b/VoteManager.cs @@ -2,19 +2,21 @@ using CounterStrikeSharp.API.Core; using CounterStrikeSharp.API.Modules.Menu; using CounterStrikeSharp.API.Modules.Timers; +using System.Reflection; using System.Text; namespace cs2_rockthevote { public class VoteManager { - public VoteManager(List maps, RockTheVote plugin, int voteDuration, int canVoteCount, int mapstoShow) + public VoteManager(List maps, RockTheVote plugin, int voteDuration, int canVoteCount, int mapstoShow, bool changeImmediatly) { Maps = maps; Plugin = plugin; Duration = voteDuration; CanVoteCount = canVoteCount; MapsToShow = mapstoShow; + ChangeImmediatly = changeImmediatly; } private List Maps { get; } @@ -22,7 +24,9 @@ public VoteManager(List maps, RockTheVote plugin, int voteDuration, int private int Duration { get; set; } public int CanVoteCount { get; } public int MapsToShow { get; } + public bool ChangeImmediatly { get; } private CounterStrikeSharp.API.Modules.Timers.Timer? Timer { get; set; } + public string? NextMap { get; private set; } Dictionary Votes = new(); int TimeLeft = 0; @@ -30,7 +34,7 @@ public VoteManager(List maps, RockTheVote plugin, int voteDuration, int public void MapVoted(CCSPlayerController player, string mapName) { Votes[mapName] += 1; - player.PrintToChat($"[RockTheVote] You voted in {mapName}"); + player.PrintToChat(Plugin.Localize("you-voted", mapName)); VoteDisplayTick(TimeLeft); if (Votes.Select(x => x.Value).Sum() >= CanVoteCount) { @@ -62,7 +66,7 @@ void VoteDisplayTick(int time) { int index = 1; StringBuilder stringBuilder = new(); - stringBuilder.AppendLine($"Vote for the next map: {time}s"); + stringBuilder.AppendLine(Plugin.Localizer["vote-for-next-map-hud", time]); foreach (var kv in Votes.OrderByDescending(x => x.Value).Take(2)) { if(kv.Value > 0) stringBuilder.AppendLine($"{index++} {kv.Key} ({kv.Value})"); @@ -71,32 +75,57 @@ void VoteDisplayTick(int time) PrintCenterTextAll(stringBuilder.ToString()); } + public bool ChangeNextMap() + { + if (string.IsNullOrWhiteSpace(NextMap)) + return false; + + var nextMap = NextMap; + Server.PrintToChatAll(Plugin.Localize("changing-map", nextMap)); + Plugin.AddTimer(3.0F, () => + { + if(Server.IsMapValid(nextMap)) + { + Server.ExecuteCommand($"changelevel {nextMap}"); + } + else + Server.ExecuteCommand($"ds_workshop_changelevel {nextMap}"); + }); + NextMap = null; + return true; + } + 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))"); + if(percent > 0) + { + Server.PrintToChatAll(Plugin.Localize("vote-ended", winner.Key, percent, totalVotes)); + } 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}"); + Server.PrintToChatAll(Plugin.Localize("vote-ended-no-votes", winner.Key)); } - PrintCenterTextAll($"Vote finished, next map: {winner.Key}"); - Plugin.AddTimer(4.0F, () => + if (!ChangeImmediatly) { - Server.ExecuteCommand($"ds_workshop_changelevel {winner.Key}"); - Server.ExecuteCommand($"changelevel {winner.Key}"); - }); + Server.PrintToChatAll(Plugin.Localize("changing-map-next-round", winner.Key)); + } + PrintCenterTextAll(Plugin.Localizer["vote-finished-hud", winner.Key]); + + NextMap = winner.Key; + if (ChangeImmediatly) + ChangeNextMap(); } public void StartVote() { - ChatMenu menu = new($"Vote for the next map:"); + ChatMenu menu = new(Plugin.Localizer["vote-for-next-map"]); foreach(var map in Maps.Take(MapsToShow)) { Votes[map] = 0; diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 0000000..7b90601 --- /dev/null +++ b/lang/en.json @@ -0,0 +1,21 @@ +{ + "prefix": "{red}[RocktheVote]{default} ", + "invalid-map": "Invalid map", + "nominate-current": "You can't nominate the current map", + "nominated": "Player {green}{0}{default} nominated map {1}, now it has {2} vote(s)", + "minimum-rounds": "Minimum rounds to use this command is {0}", + "disabled-warmup": "Command disabled during warmup.", + "minimum-players": "Minimum players to use this command is {0}", + "nomination-votes-reached": "Number of votes reached, nomination disabled", + "rocked-the-vote": "Player {green}{0}{default} wants to rock the vote ({1} voted, {2} needed)", + "already-rocked-the-vote": "You already rocked the vote ({0} voted, {1} needed)", + "starting-vote": "Player {green}{0}{default} wants to rock the vote ({1} voted, {2} needed), number of votes reached.", + "you-voted": "You voted in {0}", + "changing-map": "Changing map to {0}.", + "vote-ended": "Vote ended the next map will be {0} ({1}% of {2} vote(s))", + "vote-ended-no-votes": "No votes, the next map will be {0}", + "changing-map-next-round": "The map will be changed to {0} in the next round...", + "vote-for-next-map": "Vote for the next map:", + "vote-for-next-map-hud": "Vote for the next map: {0}s", + "vote-finished-hud": "Vote finished, next map: {0}" +} \ No newline at end of file diff --git a/lang/pt-BR.json b/lang/pt-BR.json new file mode 100644 index 0000000..4945513 --- /dev/null +++ b/lang/pt-BR.json @@ -0,0 +1,21 @@ +{ + "prefix": "{red}[RocktheVote]{default} ", + "invalid-map": "Mapa inválido", + "nominate-current": "Você não pode indicar o mapa atual", + "nominated": "O jogador {green}{0}{default} indicou o mapa {1}, agora ele tem {2} voto(s)", + "minimum-rounds": "O número mínimo de rodadas para usar este comando é {0}", + "disabled-warmup": "Comando desativado durante o aquecimento.", + "minimum-players": "Número mínimo de jogadores para usar este comando é {0}", + "nomination-votes-reached": "Número de votos atingido, indicação desativada", + "rocked-the-vote": "O jogador {green}{0}{default} quer iniciar a votação ({1} votaram, {2} necessário(s))", + "already-rocked-the-vote": "Você já iniciou a votação ({0} votaram, {1} necessário(s))", + "starting-vote": "O jogador {green}{0}{default} quer iniciar a votação ({1} votaram, {2} necessário(s)), número de votos atingido.", + "you-voted": "Você votou em {0}", + "changing-map": "Mudando o mapa para {0}.", + "vote-ended": "Votação encerrada, o próximo mapa será {0} ({1}% de {2} voto(s))", + "vote-ended-no-votes": "Sem votos, o próximo mapa será {0}", + "changing-map-next-round": "O mapa será alterado para {0} na próxima rodada...", + "vote-for-next-map": "Vote para o próximo mapa:", + "vote-for-next-map-hud": "Vote para o próximo mapa: {0}s", + "vote-finished-hud": "Votação encerrada, próximo mapa: {0}" +}