using System; using System.Collections.Generic; using System.Linq; using Oxide.Core; using Oxide.Core.Plugins; using Oxide.Core.Libraries.Covalence; using Newtonsoft.Json; using UnityEngine; using System.Threading.Tasks; // ____ _ _ _ // / ___|(_) __ _(_) | ___ // \___ \| |/ _` | | |/ _ \ // ___) | | (_| | | | (_) | // |____/|_|\__, |_|_|\___/ // |___/ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ // ✦ RUST PLUGINS ✦ // ✦ Sigilo.dev ✦ // ☽✧✵✧✵✧✵✧✵✧✵✧✵✧☾ namespace Oxide.Plugins { [Info("VoteMute", "Sigilo", "1.2.1")] [Description("Player voting system for muting players")] class VoteMute : RustPlugin { #region Configuration private Configuration config; class Configuration { [JsonProperty("Required percentage (0-100)")] public float RequiredPercentage = 60f; [JsonProperty("Vote duration (seconds)")] public int VoteDuration = 120; [JsonProperty("Cooldown between votes (minutes)")] public int CooldownMinutes = 10; [JsonProperty("Mute duration (minutes)")] public int MuteDuration = 60; [JsonProperty("Discord Webhook URL")] public string WebhookUrl = ""; [JsonProperty("Log to console")] public bool LogToConsole = false; [JsonProperty("Minimum players required for voting")] public int MinimumPlayers = 4; [JsonProperty("Allow voting to mute self")] public bool AllowSelfVote = true; [JsonProperty("Allow voting to mute admins")] public bool AllowAdminVote = false; [JsonProperty("Reminder interval (seconds)")] public int ReminderInterval = 20; [JsonProperty("Count only voters (if false, non-voters count as No)")] public bool CountOnlyVoters = true; } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject(); if (config == null) LoadDefaultConfig(); } catch { PrintError("Configuration file is corrupt, using defaults"); LoadDefaultConfig(); } } protected override void LoadDefaultConfig() => config = new Configuration(); protected override void SaveConfig() => Config.WriteObject(config); #endregion #region Initialization private const string permStartVote = "votemute.startvote"; private const string permVote = "votemute.vote"; private const string permUnmute = "votemute.unmute"; private Dictionary lastVoteCall = new Dictionary(); private VoteSession activeVote; private Dictionary mutedPlayers = new Dictionary(); void Init() { permission.RegisterPermission(permStartVote, this); permission.RegisterPermission(permVote, this); permission.RegisterPermission(permUnmute, this); LoadDefaultMessages(); LoadMuteData(); timer.Every(60f, CheckMutes); } void Unload() { SaveMuteData(); } protected override void LoadDefaultMessages() { var messages = new Dictionary { ["VoteStarted"] = "{2} started a vote to mute {0} ({1}m)", ["VoteDuration"] = "Vote ends in {0}s", ["VoteInstructions"] = "Type /yes or /no to vote", ["VoteSuccess"] = "{3} muted for {4}m ({0}/{1} votes, {2}%)", ["VoteFailed"] = "Vote failed for {3} ({0}/{1} votes, {2}%)", ["Cooldown"] = "Wait {0}m before starting new vote", ["NoPermission"] = "No permission to start vote", ["InvalidTarget"] = "Player '{0}' not found", ["VoteInProgress"] = "Vote already in progress", ["InvalidCommandMute"] = "Usage: /votemute ", ["MinimumPlayers"] = "Need at least {0} players online to start a vote", ["CannotVoteAdmin"] = "Cannot vote to mute admins", ["CannotVoteSelf"] = "Cannot vote to mute yourself", ["Muted"] = "You are muted", ["MutedRemaining"] = "You are muted, {0}m remaining", ["MuteExpired"] = "Your mute has expired", ["UnmuteSuccess"] = "Unmuted {0}", ["NotMuted"] = "{0} is not muted", ["NoVotePermission"] = "You don't have permission to participate in votes", ["NoUnmutePermission"] = "You don't have permission to unmute players", ["VoteReminder"] = "Vote to mute {0} - {1}s left", ["AlreadyVoted"] = "You have already voted" }; lang.RegisterMessages(messages, this, "en"); lang.RegisterMessages(messages, this); } #endregion #region Voting System class VoteSession { public string TargetName; public ulong TargetId; public Dictionary Votes = new Dictionary(); public Timer VoteTimer; public Timer ReminderTimer; public int RemainingTime; } [ChatCommand("votemute")] void CmdVoteMute(BasePlayer player, string command, string[] args) { StartVote(player, args); } void StartVote(BasePlayer initiator, string[] args) { if (!permission.UserHasPermission(initiator.UserIDString, permStartVote)) { SendReply(initiator, Lang("NoPermission", initiator.UserIDString)); return; } if (BasePlayer.activePlayerList.Count < config.MinimumPlayers) { SendReply(initiator, Lang("MinimumPlayers", initiator.UserIDString, config.MinimumPlayers)); return; } if (activeVote != null) { SendReply(initiator, Lang("VoteInProgress", initiator.UserIDString)); return; } if (args.Length == 0) { SendReply(initiator, Lang("InvalidCommandMute", initiator.UserIDString)); return; } if (lastVoteCall.TryGetValue(initiator.userID, out DateTime lastCall) && (DateTime.Now - lastCall).TotalMinutes < config.CooldownMinutes) { var remainingTime = config.CooldownMinutes - (DateTime.Now - lastCall).TotalMinutes; SendReply(initiator, Lang("Cooldown", initiator.UserIDString, Math.Round(remainingTime, 2))); return; } var target = FindPlayer(args[0]); if (target == null) { SendReply(initiator, Lang("InvalidTarget", initiator.UserIDString, args[0])); return; } if (!config.AllowSelfVote && target.userID == initiator.userID) { SendReply(initiator, Lang("CannotVoteSelf", initiator.UserIDString)); return; } if (!config.AllowAdminVote && target.IsAdmin) { SendReply(initiator, Lang("CannotVoteAdmin", initiator.UserIDString)); return; } activeVote = new VoteSession { TargetName = target.displayName, TargetId = target.userID, RemainingTime = config.VoteDuration, VoteTimer = timer.Once(config.VoteDuration, EndVote) }; lastVoteCall[initiator.userID] = DateTime.Now; BroadcastChat(Lang("VoteStarted", null, target.displayName, config.MuteDuration, initiator.displayName)); BroadcastChat(Lang("VoteDuration", null, config.VoteDuration)); BroadcastChat(Lang("VoteInstructions")); if (config.ReminderInterval > 0 && config.ReminderInterval < config.VoteDuration) { activeVote.RemainingTime = config.VoteDuration; activeVote.ReminderTimer = timer.Every(config.ReminderInterval, () => { activeVote.RemainingTime -= config.ReminderInterval; if (activeVote.RemainingTime <= 5) return; BroadcastChat(Lang("VoteReminder", null, activeVote.TargetName, activeVote.RemainingTime)); BroadcastChat(Lang("VoteInstructions")); }); } } [ChatCommand("yes")] void VoteYes(BasePlayer player, string command, string[] args) { RegisterVote(player, true); } [ChatCommand("no")] void VoteNo(BasePlayer player, string command, string[] args) { RegisterVote(player, false); } void RegisterVote(BasePlayer voter, bool vote) { if (activeVote == null) return; if (!permission.UserHasPermission(voter.UserIDString, permVote)) { SendReply(voter, Lang("NoVotePermission", voter.UserIDString)); return; } if (activeVote.Votes.ContainsKey(voter.userID)) { SendReply(voter, Lang("AlreadyVoted", voter.UserIDString)); return; } activeVote.Votes[voter.userID] = vote; SendReply(voter, $"You voted {(vote ? "YES" : "NO")}"); } void EndVote() { if (activeVote == null) return; if (activeVote.ReminderTimer != null) { activeVote.ReminderTimer.Destroy(); } int participants; float votePercentage; var yesVotes = activeVote.Votes.Count(v => v.Value); var totalVotes = activeVote.Votes.Count; if (config.CountOnlyVoters) { participants = totalVotes > 0 ? totalVotes : 1; votePercentage = (yesVotes * 100.0f) / participants; } else { participants = BasePlayer.activePlayerList.Count; votePercentage = (yesVotes * 100.0f) / participants; } var requiredVotes = (int)Math.Ceiling(participants * (config.RequiredPercentage / 100)); try { if (yesVotes >= requiredVotes) { ExecuteMute(activeVote); BroadcastChat(Lang("VoteSuccess", null, yesVotes, participants, Math.Round(votePercentage, 1), activeVote.TargetName, config.MuteDuration)); SendDiscordEmbed("Vote Passed", $"Player **{activeVote.TargetName}** has been muted.", 3066993, new List> { new Dictionary { { "name", "Player ID" }, { "value", activeVote.TargetId.ToString() }, { "inline", true } }, new Dictionary { { "name", "Mute Duration" }, { "value", $"{config.MuteDuration} minutes" }, { "inline", true } }, new Dictionary { { "name", "Vote Result" }, { "value", $"{yesVotes}/{participants} ({Math.Round(votePercentage, 1)}%)" }, { "inline", true } }, new Dictionary { { "name", "Count Mode" }, { "value", config.CountOnlyVoters ? "Only Voters" : "All Players" }, { "inline", true } }, new Dictionary { { "name", "Server" }, { "value", ConVar.Server.hostname }, { "inline", true } } } ); } else { BroadcastChat(Lang("VoteFailed", null, yesVotes, participants, Math.Round(votePercentage, 1), activeVote.TargetName)); SendDiscordEmbed("Vote Failed", $"Failed to mute player **{activeVote.TargetName}**.", 15158332, new List> { new Dictionary { { "name", "Vote Result" }, { "value", $"{yesVotes}/{participants} ({Math.Round(votePercentage, 1)}%)" }, { "inline", true } }, new Dictionary { { "name", "Count Mode" }, { "value", config.CountOnlyVoters ? "Only Voters" : "All Players" }, { "inline", true } }, new Dictionary { { "name", "Server" }, { "value", ConVar.Server.hostname }, { "inline", true } } } ); } } catch (Exception ex) { PrintError($"Error in EndVote: {ex.Message}"); } finally { activeVote = null; } } void ExecuteMute(VoteSession vote) { try { mutedPlayers[vote.TargetId] = DateTime.Now.AddMinutes(config.MuteDuration); var target = BasePlayer.FindByID(vote.TargetId); if (target != null) { target.IPlayer.Reply(Lang("Muted", target.UserIDString)); target.SendNetworkUpdate(); } SaveMuteData(); } catch (Exception ex) { PrintError($"Error in ExecuteMute: {ex.Message}"); } } void CheckMutes() { var now = DateTime.Now; foreach (var entry in mutedPlayers.Where(entry => entry.Value <= now).ToList()) { mutedPlayers.Remove(entry.Key); var player = BasePlayer.FindByID(entry.Key); player?.IPlayer.Reply(Lang("MuteExpired", player.UserIDString)); } SaveMuteData(); } #endregion #region Helper Methods BasePlayer FindPlayer(string search) { return BasePlayer.activePlayerList .FirstOrDefault(p => p.displayName.Contains(search, StringComparison.OrdinalIgnoreCase) || p.UserIDString == search); } string Lang(string key, string userId = null, params object[] args) { try { var message = lang.GetMessage(key, this, userId); return args == null || args.Length == 0 ? message : string.Format(message, args); } catch (Exception ex) { PrintError($"Lang format error for key '{key}': {ex.Message}"); return key; } } void BroadcastChat(string message) { foreach (var player in BasePlayer.activePlayerList) SendReply(player, message); if (config.LogToConsole) Puts(message); } async Task SendDiscordEmbed(string title, string description, int color, List> fields) { var embed = new Dictionary { {"title", title}, {"description", description}, {"color", color}, {"timestamp", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")} }; if (fields != null && fields.Count > 0) { embed["fields"] = fields; } var payloadObj = new Dictionary { {"username", "Vote Mute System"}, {"embeds", new object[] { embed } } }; var payload = JsonConvert.SerializeObject(payloadObj); try { webrequest.EnqueuePost( config.WebhookUrl, payload, (code, response) => { if (code != 204 && code != 200) PrintError($"Discord webhook error: {code} - {response}"); }, this, new Dictionary { ["Content-Type"] = "application/json" } ); } catch (Exception ex) { PrintError($"Discord embed log failed: {ex.Message}"); } } void LoadMuteData() { mutedPlayers = Interface.Oxide.DataFileSystem.ReadObject>(Name) ?? new Dictionary(); } void SaveMuteData() { Interface.Oxide.DataFileSystem.WriteObject(Name, mutedPlayers); } #endregion #region Hooks [PluginReference] private Plugin BetterChat; private readonly HashSet mutedReplied = new HashSet(); private bool MuteCheck(IPlayer player, bool sendMessage = true) { ulong id = ulong.Parse(player.Id); if (mutedPlayers.TryGetValue(id, out DateTime muteExpiry)) { if (DateTime.Now >= muteExpiry) { mutedPlayers.Remove(id); SaveMuteData(); return false; } if (sendMessage && !mutedReplied.Contains(id)) { int timeRemaining = (int)Math.Ceiling((muteExpiry - DateTime.Now).TotalMinutes); if (timeRemaining < 1) timeRemaining = 1; mutedReplied.Add(id); player.Reply(string.Format(Lang("MutedRemaining", player.Id), timeRemaining)); timer.Once(0.5f, () => mutedReplied.Remove(id)); } return true; } return false; } object OnPlayerVoice(BasePlayer player) { if (MuteCheck(player.IPlayer, sendMessage: false)) return true; return null; } object OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel) { if (BetterChat != null) return null; if (MuteCheck(player.IPlayer)) return false; return null; } object OnUserChat(IPlayer player, string message) { if (MuteCheck(player)) return true; return null; } void OnBetterChat(Dictionary data) { IPlayer player = data["Player"] as IPlayer; if (player == null) return; if (MuteCheck(player, sendMessage: false)) data["CancelOption"] = 2; } object OnTeamChat(BasePlayer player, string message) { if (MuteCheck(player.IPlayer)) return false; return null; } void OnPlayerDisconnected(BasePlayer player) { if (activeVote?.TargetId == player.userID) EndVote(); } #endregion [ChatCommand("unmute")] void CmdUnmute(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, permUnmute)) { SendReply(player, Lang("NoUnmutePermission", player.UserIDString)); return; } if (args.Length == 0) { SendReply(player, Lang("InvalidTarget", player.UserIDString)); return; } var target = FindPlayer(args[0]); if (target == null) { SendReply(player, Lang("InvalidTarget", player.UserIDString, args[0])); return; } if (!mutedPlayers.ContainsKey(target.userID)) { SendReply(player, Lang("NotMuted", player.UserIDString, target.displayName)); return; } mutedPlayers.Remove(target.userID); SaveMuteData(); string message = Lang("UnmuteSuccess", player.UserIDString, target.displayName); SendReply(player, message); SendDiscordEmbed("Player Unmuted", $"Admin **{player.displayName}** unmuted player **{target.displayName}**.", 3447003, new List> { new Dictionary { { "name", "Admin" }, { "value", player.displayName }, { "inline", true } }, new Dictionary { { "name", "Player" }, { "value", target.displayName }, { "inline", true } } } ); } } }