C#:
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<Configuration>();
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<ulong, DateTime> lastVoteCall = new Dictionary<ulong, DateTime>();
private VoteSession activeVote;
private Dictionary<ulong, DateTime> mutedPlayers = new Dictionary<ulong, DateTime>();
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<string, string>
{
["VoteStarted"] = "<color=#E6BE8A>{2}</color> started a vote to mute <color=#B87333>{0}</color> ({1}m)",
["VoteDuration"] = "Vote ends in <color=#9CAF88>{0}</color>s",
["VoteInstructions"] = "Type <color=#E6BE8A>/yes</color> or <color=#E6BE8A>/no</color> to vote",
["VoteSuccess"] = "<color=#B87333>{3}</color> muted for {4}m (<color=#9CAF88>{0}/{1}</color> votes, {2}%)",
["VoteFailed"] = "Vote failed for <color=#B87333>{3}</color> (<color=#9CAF88>{0}/{1}</color> votes, {2}%)",
["Cooldown"] = "Wait {0}m before starting new vote",
["NoPermission"] = "No permission to start vote",
["InvalidTarget"] = "Player '<color=#B87333>{0}</color>' not found",
["VoteInProgress"] = "Vote already in progress",
["InvalidCommandMute"] = "Usage: <color=#E6BE8A>/votemute</color> <color=#B87333><player></color>",
["MinimumPlayers"] = "Need at least <color=#9CAF88>{0}</color> 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, <color=#9CAF88>{0}</color>m remaining",
["MuteExpired"] = "<color=#708090>Your mute has expired</color>",
["UnmuteSuccess"] = "Unmuted <color=#B87333>{0}</color>",
["NotMuted"] = "<color=#B87333>{0}</color> 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 <color=#B87333>{0}</color> - {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<ulong, bool> Votes = new Dictionary<ulong, bool>();
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 ? "<color=#E6BE8A>YES</color>" : "<color=#708090>NO</color>")}");
}
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<Dictionary<string, object>>
{
new Dictionary<string, object> { { "name", "Player ID" }, { "value", activeVote.TargetId.ToString() }, { "inline", true } },
new Dictionary<string, object> { { "name", "Mute Duration" }, { "value", $"{config.MuteDuration} minutes" }, { "inline", true } },
new Dictionary<string, object> { { "name", "Vote Result" }, { "value", $"{yesVotes}/{participants} ({Math.Round(votePercentage, 1)}%)" }, { "inline", true } },
new Dictionary<string, object> { { "name", "Count Mode" }, { "value", config.CountOnlyVoters ? "Only Voters" : "All Players" }, { "inline", true } },
new Dictionary<string, object> { { "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<Dictionary<string, object>>
{
new Dictionary<string, object> { { "name", "Vote Result" }, { "value", $"{yesVotes}/{participants} ({Math.Round(votePercentage, 1)}%)" }, { "inline", true } },
new Dictionary<string, object> { { "name", "Count Mode" }, { "value", config.CountOnlyVoters ? "Only Voters" : "All Players" }, { "inline", true } },
new Dictionary<string, object> { { "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<Dictionary<string, object>> fields)
{
var embed = new Dictionary<string, object>
{
{"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<string, object>
{
{"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<string, string>
{
["Content-Type"] = "application/json"
}
);
}
catch (Exception ex)
{
PrintError($"Discord embed log failed: {ex.Message}");
}
}
void LoadMuteData()
{
mutedPlayers = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, DateTime>>(Name) ?? new Dictionary<ulong, DateTime>();
}
void SaveMuteData()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, mutedPlayers);
}
#endregion
#region Hooks
[PluginReference]
private Plugin BetterChat;
private readonly HashSet<ulong> mutedReplied = new HashSet<ulong>();
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<string, object> 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<Dictionary<string, object>>
{
new Dictionary<string, object> { { "name", "Admin" }, { "value", player.displayName }, { "inline", true } },
new Dictionary<string, object> { { "name", "Player" }, { "value", target.displayName }, { "inline", true } }
}
);
}
}
}