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("VoteBan", "Sigilo", "1.1.1")] [Description("Player voting system for banning players")] class VoteBan : RustPlugin { #region Configuration private Configuration config; class Configuration { [JsonProperty("Required percentage (0-100)")] public float RequiredPercentage = 90f; [JsonProperty("Vote duration (seconds)")] public int VoteDuration = 120; [JsonProperty("Cooldown between votes (minutes)")] public int CooldownMinutes = 10; [JsonProperty("Cooldown between instant bans (minutes)")] public int InstantBanCooldownMinutes = 5; [JsonProperty("Ban duration (minutes, 0 for permanent)")] public int BanDuration = 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 = 9; [JsonProperty("Allow voting to ban self")] public bool AllowSelfVote = false; [JsonProperty("Allow voting to ban admins")] public bool AllowAdminVote = false; [JsonProperty("Reminder interval (seconds)")] public int ReminderInterval = 30; [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 = "voteban.start"; private const string permVote = "voteban.vote"; private const string permInstantBan = "voteban.instantban"; private Dictionary lastVoteCall = new Dictionary(); private Dictionary lastInstantBanCall = new Dictionary(); private VoteSession activeVote; private Dictionary tempBans = new Dictionary(); void Init() { permission.RegisterPermission(permStartVote, this); permission.RegisterPermission(permVote, this); permission.RegisterPermission(permInstantBan, this); LoadDefaultMessages(); LoadBanData(); timer.Every(60f, CheckTempBans); } void Unload() { SaveBanData(); } protected override void LoadDefaultMessages() { var messages = new Dictionary { ["VoteStarted"] = "A vote has started to ban {0} for {1}!", ["VoteDuration"] = "Vote will last {0} seconds", ["VoteInstructions"] = "Type /y to ban or /n to cancel", ["VoteSuccess"] = "Vote passed ({0}/{1} votes): {2} has been banned for {3}", ["VoteFailed"] = "Vote failed ({0}/{1} votes): {2} will not be banned", ["Cooldown"] = "Wait {0} minutes before starting another vote", ["NoPermission"] = "You don't have permission to start a vote", ["InvalidTarget"] = "Player '{0}' not found", ["VoteInProgress"] = "A vote is already in progress", ["InvalidCommandBan"] = "Usage: /voteban ", ["MinimumPlayers"] = "Need at least {0} players online to start a vote", ["CannotVoteAdmin"] = "Cannot vote to ban admin", ["CannotVoteSelf"] = "Cannot vote to ban yourself", ["AlreadyBanned"] = "{0} is already banned", ["BanSuccessOnline"] = "You have been banned via vote", ["NoVotePermission"] = "You don't have permission to vote", ["VoteReminder"] = "Vote to ban {0} - {1}s left", ["AlreadyVoted"] = "You have already voted", ["InstantBanNoPermission"] = "You don't have permission to instant ban players", ["InstantBanSuccess"] = "{0} has been banned for {1} by {2}", ["InstantBanCooldown"] = "Wait {0} minutes before using instant ban again", ["InvalidCommandInstantBan"] = "Usage: /instaban " }; 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("voteban")] void CmdVoteBan(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("InvalidCommandBan", 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 (tempBans.ContainsKey(target.userID)) { SendReply(initiator, Lang("AlreadyBanned", initiator.UserIDString, target.displayName)); 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; string banDurationText = config.BanDuration > 0 ? $"{config.BanDuration} minutes" : "permanently"; BroadcastChat(Lang("VoteStarted", null, target.displayName, banDurationText)); 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("y")] void VoteYes(BasePlayer player, string command, string[] args) { RegisterVote(player, true); } [ChatCommand("n")] void VoteNo(BasePlayer player, string command, string[] args) { RegisterVote(player, false); } [ChatCommand("instaban")] void CmdInstantBan(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, permInstantBan)) { SendReply(player, Lang("InstantBanNoPermission", player.UserIDString)); return; } if (args.Length == 0) { SendReply(player, Lang("InvalidCommandInstantBan", player.UserIDString)); return; } if (lastInstantBanCall.TryGetValue(player.userID, out DateTime lastCall) && (DateTime.Now - lastCall).TotalMinutes < config.InstantBanCooldownMinutes) { var remainingTime = config.InstantBanCooldownMinutes - (DateTime.Now - lastCall).TotalMinutes; SendReply(player, Lang("InstantBanCooldown", player.UserIDString, Math.Round(remainingTime, 2))); return; } var target = FindPlayer(args[0]); if (target == null) { SendReply(player, Lang("InvalidTarget", player.UserIDString, args[0])); return; } if (tempBans.ContainsKey(target.userID)) { SendReply(player, Lang("AlreadyBanned", player.UserIDString, target.displayName)); return; } if (!config.AllowAdminVote && target.IsAdmin) { SendReply(player, Lang("CannotVoteAdmin", player.UserIDString)); return; } lastInstantBanCall[player.userID] = DateTime.Now; ExecuteInstantBan(target, player); } void RegisterVote(BasePlayer voter, bool vote) { if (activeVote == null) { SendReply(voter, Lang("VoteInProgress", voter.UserIDString)); 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) { ExecuteBan(activeVote, yesVotes, participants, votePercentage); string banDurationText = config.BanDuration > 0 ? $"{config.BanDuration} minutes" : "permanently"; BroadcastChat(Lang("VoteSuccess", null, yesVotes, participants, activeVote.TargetName, banDurationText)); } else { BroadcastChat(Lang("VoteFailed", null, yesVotes, participants, activeVote.TargetName)); } } catch (Exception ex) { PrintError($"Error in EndVote: {ex.Message}"); } finally { activeVote = null; } } void ExecuteInstantBan(BasePlayer target, BasePlayer initiator) { try { IPlayer targetIPlayer = covalence.Players.FindPlayer(target.userID.ToString()); string reason = config.BanDuration > 0 ? $"Instantly banned by {initiator.displayName} for {config.BanDuration} minutes" : $"Permanently banned by {initiator.displayName}"; if (config.BanDuration > 0) { tempBans[target.userID] = DateTime.Now.AddMinutes(config.BanDuration); ConsoleSystem.Run(ConsoleSystem.Option.Server, $"banid {target.userID} {config.BanDuration * 60} \"{reason}\""); } else { ConsoleSystem.Run(ConsoleSystem.Option.Server, $"banid {target.userID} \"{reason}\""); } if (target != null) { target.Kick(reason); } SaveBanData(); string banDurationText = config.BanDuration > 0 ? $"{config.BanDuration} minutes" : "permanently"; BroadcastChat(Lang("InstantBanSuccess", null, target.displayName, banDurationText, initiator.displayName)); SendDiscordEmbed("Instant Ban", $"Player **{target.displayName}** has been instantly banned by **{initiator.displayName}**.", 15158332, new List> { new Dictionary { { "name", "Player Name" }, { "value", target.displayName }, { "inline", true } }, new Dictionary { { "name", "Player ID" }, { "value", target.userID.ToString() }, { "inline", true } }, new Dictionary { { "name", "Banned By" }, { "value", initiator.displayName }, { "inline", true } }, new Dictionary { { "name", "Ban Duration" }, { "value", config.BanDuration > 0 ? $"{config.BanDuration} minutes" : "Permanent" }, { "inline", true } } } ); } catch (Exception ex) { PrintError($"Error in ExecuteInstantBan: {ex.Message}"); } } void ExecuteBan(VoteSession vote, int yesVotes, int participants, float votePercentage) { try { IPlayer targetIPlayer = covalence.Players.FindPlayer(vote.TargetId.ToString()); string reason = config.BanDuration > 0 ? $"Banned by vote for {config.BanDuration} minutes" : "Permanently banned by vote"; if (targetIPlayer != null) { if (config.BanDuration > 0) { tempBans[vote.TargetId] = DateTime.Now.AddMinutes(config.BanDuration); ConsoleSystem.Run(ConsoleSystem.Option.Server, $"banid {vote.TargetId} {config.BanDuration * 60} \"{reason}\""); } else { ConsoleSystem.Run(ConsoleSystem.Option.Server, $"banid {vote.TargetId} \"{reason}\""); } var basePlayer = BasePlayer.FindByID(vote.TargetId); if (basePlayer != null) { basePlayer.Kick(reason); } } else { if (config.BanDuration > 0) { tempBans[vote.TargetId] = DateTime.Now.AddMinutes(config.BanDuration); ConsoleSystem.Run(ConsoleSystem.Option.Server, $"banid {vote.TargetId} {config.BanDuration * 60} \"{reason}\""); } else { ConsoleSystem.Run(ConsoleSystem.Option.Server, $"banid {vote.TargetId} \"{reason}\""); } } SaveBanData(); SendDiscordEmbed("Vote Ban Success", $"Player **{vote.TargetName}** has been banned via vote.", 3066993, new List> { new Dictionary { { "name", "Player Name" }, { "value", vote.TargetName }, { "inline", true } }, new Dictionary { { "name", "Player ID" }, { "value", vote.TargetId.ToString() }, { "inline", true } }, new Dictionary { { "name", "Ban Duration" }, { "value", config.BanDuration > 0 ? $"{config.BanDuration} minutes" : "Permanent" }, { "inline", true } }, new Dictionary { { "name", "Vote Result" }, { "value", $"{vote.Votes.Count(v => v.Value)}/{participants} ({Math.Round(votePercentage, 1)}%)" }, { "inline", true } }, new Dictionary { { "name", "Count Mode" }, { "value", config.CountOnlyVoters ? "Only Voters" : "All Players" }, { "inline", true } } } ); } catch (Exception ex) { PrintError($"Error in ExecuteBan: {ex.Message}"); } } #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) { if (string.IsNullOrEmpty(config.WebhookUrl)) return; 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 Ban System"}, {"embeds", new object[] { embed } } }; var payload = JsonConvert.SerializeObject(payloadObj); try { webrequest.EnqueuePost( config.WebhookUrl.TrimEnd('/'), 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 LoadBanData() { tempBans = Interface.Oxide.DataFileSystem.ReadObject>(Name) ?? new Dictionary(); } void SaveBanData() { Interface.Oxide.DataFileSystem.WriteObject(Name, tempBans); } #endregion void CheckTempBans() { var now = DateTime.Now; foreach (var entry in tempBans.Where(entry => entry.Value <= now).ToList()) { try { ConsoleSystem.Run(ConsoleSystem.Option.Server, $"unban {entry.Key}"); var targetName = covalence.Players.FindPlayerById(entry.Key.ToString())?.Name ?? "Unknown"; SendDiscordEmbed("Temporary Ban Expired", $"Player **{targetName}** has been unbanned.", 3447003, new List> { new Dictionary { { "name", "Player Name" }, { "value", targetName }, { "inline", true } }, new Dictionary { { "name", "Player ID" }, { "value", entry.Key.ToString() }, { "inline", true } } } ); } catch (Exception ex) { PrintError($"Error unbanning player with ID {entry.Key}: {ex.Message}"); } tempBans.Remove(entry.Key); } SaveBanData(); } } }