C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections.Generic;
using System.Net;
using System.Text.RegularExpressions;
using System.Text;
using Newtonsoft.Json;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Libraries;
using Oxide.Core.Plugins;
using UnityEngine;
namespace Oxide.Plugins {
[Info("rServerMessages", "Ftuoil Xelrash", "0.0.261")]
[Description("Logs essential server events to Discord channels using webhooks")]
public class rServerMessages : RustPlugin {
#region Variables
[PluginReference]
private readonly Plugin AntiSpam, BetterChatMute, UFilter;
private readonly Queue<QueuedMessage> _queue = new();
private readonly StringBuilder _sb = new();
private int _retryCount = 0;
private const int _maxRetries = 5;
// Maximum retry attempts before giving up
private object _resultCall;
private QueuedMessage _nextMessage;
private QueuedMessage _queuedMessage;
private string[] _profanities;
private Timer _timerQueue;
private Timer _timerQueueCooldown;
private bool _isProcessingQueue = false;
// Prevent recursive queue processing
private readonly List<Regex> _regexTags = new() {
new("<color=.+?>", RegexOptions.Compiled), new("<size=.+?>", RegexOptions.Compiled)
}
;
private readonly List<string> _tags = new() {
"</color>", "</size>", "<i>", "</i>", "<b>", "</b>"
}
;
// Simple entity type mappings for basic death messages
private readonly Dictionary<string, string> _entityNames = new() {
// Players
["BasePlayer"]
= "Player", // NPCs
["Scientist"]
= "Scientist", ["ScientistNPC"] = "Scientist", ["HTNPlayer"] = "Heavy Scientist", ["NPCMurderer"] = "Murderer", ["scarecrow"] = "Scarecrow", ["ScientistNPCNew"] = "Scientist", ["tunneldweller"] = "Tunnel Dweller", ["underwaterdweller"] = "Underwater Dweller", ["ZombieNPC"] = "Zombie", ["GingerbreadNPC"] = "Gingerbread Man", ["NPCShopKeeper"] = "Shopkeeper", // Animals
["Bear"]
= "Bear", ["Boar"] = "Boar", ["Chicken"] = "Chicken", ["Stag"] = "Stag", ["Wolf"] = "Wolf", ["Wolf2"] = "Wolf", ["Polarbear"] = "Polar Bear", ["Horse"] = "Horse", ["RidableHorse"] = "Horse", ["FarmableAnimal"] = "Farm Animal", ["Crocodile"] = "Crocodile", ["SnakeHazard"] = "Snake", ["Panther"] = "Panther", ["Tiger"] = "Tiger", ["SimpleShark"] = "Shark", ["BaseFishNPC"] = "Fish", // Vehicles/Military
["BaseHelicopter"]
= "Patrol Helicopter", ["PatrolHelicopter"] = "Patrol Helicopter", ["BradleyAPC"] = "Bradley APC", ["Minicopter"] = "Minicopter", ["ScrapTransportHelicopter"] = "Scrap Helicopter", ["AttackHelicopter"] = "Attack Helicopter", // Turrets/Traps
["AutoTurret"]
= "Auto Turret", ["GunTrap"] = "Shotgun Trap", ["FlameTurret"] = "Flame Turret", ["NPCAutoTurret"] = "Sentry Turret", ["SamSite"] = "SAM Site", ["Landmine"] = "Landmine", ["BearTrap"] = "Bear Trap", ["TeslaCoil"] = "Tesla Coil", // Structures
["Barricade"]
= "Barricade", ["SimpleBuildingBlock"] = "Wall", ["CodeLock"] = "Code Lock", // Fire/Heat
["FireBall"]
= "Fire", ["BaseOven"] = "Heat Source", ["Campfire"] = "Campfire", // Other
["BeeSwarmAI"]
= "Bee Swarm"
}
;
private class QueuedMessage {
public string WebhookUrl {
get;
set;
}
public string Message {
get;
set;
}
public DiscordMessage DiscordMessage {
get;
set;
}
public bool IsEmbed {
get;
set;
}
}
#endregion Variables
#region Initialization
private void Init() {
UnsubscribeHooks();
}
private void Unload() {
_timerQueue?.Destroy();
_timerQueueCooldown?.Destroy();
}
private void OnServerInitialized(bool isStartup) {
if (isStartup && _configData.ServerStateSettings.Enabled) {
DiscordSendMessage(Lang(LangKeys.Event.Initialized), _configData.GlobalSettings.ServerMessagesWebhook);
}
SubscribeHooks();
}
private void OnServerShutdown() {
if (_configData.ServerStateSettings.Enabled) {
string url = _configData.GlobalSettings.ServerMessagesWebhook;
if (!string.IsNullOrEmpty(url)) {
webrequest.Enqueue(url, new DiscordMessage(Lang(LangKeys.Event.Shutdown)).ToJson(), DiscordSendMessageCallback, null, RequestMethod.POST, _headers);
}
}
}
#endregion Initialization
#region Configuration
private ConfigData _configData;
private class ConfigData {
[JsonProperty(PropertyName = "Global settings")]
public GlobalSettings GlobalSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Air Event settings")]
public EventSettings AirEventSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Airfield Event settings")]
public EventSettings AirfieldEventSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Arctic Base Event settings")]
public EventSettings ArcticBaseEventSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Armored Train Event settings")]
public EventSettings ArmoredTrainEventSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Chat settings")]
public EventSettings ChatSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Chat (Team) settings")]
public EventSettings ChatTeamSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Christmas settings")]
public EventSettings ChristmasSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Easter settings")]
public EventSettings EasterSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Gas Station Event settings")]
public EventSettings GasStationEventSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Halloween settings")]
public EventSettings HalloweenSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Permissions settings")]
public EventSettings PermissionsSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Player death settings")]
public DeathSettings PlayerDeathSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Player connect advanced info settings")]
public EventSettings PlayerConnectedInfoSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Player disconnect settings")]
public EventSettings PlayerDisconnectedSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Raidable Bases settings")]
public EventSettings RaidableBasesSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Rcon command settings")]
public EventSettings RconCommandSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Rcon connection settings")]
public EventSettings RconConnectionSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "SantaSleigh settings")]
public EventSettings SantaSleighSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Server messages settings")]
public EventSettings ServerMessagesSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Server state settings")]
public EventSettings ServerStateSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Sputnik Event settings")]
public EventSettings SputnikEventSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "Supermarket Event settings")]
public EventSettings SupermarketEventSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "User Banned settings")]
public EventSettings UserBannedSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "User Kicked settings")]
public EventSettings UserKickedSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "User Muted settings")]
public EventSettings UserMutedSettings {
get;
set;
}
= new();
[JsonProperty(PropertyName = "User Name Updated settings")]
public EventSettings UserNameUpdateSettings {
get;
set;
}
= new();
}
private class GlobalSettings {
[JsonProperty(PropertyName = "Log to console?")]
public bool LoggingEnabled {
get;
set;
}
= false;
[JsonProperty(PropertyName = "Use AntiSpam plugin on chat messages")]
public bool UseAntiSpam {
get;
set;
}
= false;
[JsonProperty(PropertyName = "Use UFilter plugin on chat messages")]
public bool UseUFilter {
get;
set;
}
= false;
[JsonProperty(PropertyName = "Hide admin connect/disconnect messages")]
public bool HideAdmin {
get;
set;
}
= false;
[JsonProperty(PropertyName = "Hide NPC death messages")]
public bool HideNPC {
get;
set;
}
= false;
[JsonProperty(PropertyName = "Include death coordinates in death messages")]
public bool IncludeDeathCoordinates {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Use Discord Embeds for death messages")]
public bool UseEmbedForDeaths {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Use enhanced embeds for connections")]
public bool UseEmbedForConnections {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Show country information (requires internet)")]
public bool ShowCountryInfo {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Show server population in connection messages")]
public bool ShowServerPopulation {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Show combat details in death messages")]
public bool ShowCombatDetails {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Use enhanced embeds for server messages")]
public bool UseEmbedForServerMessages {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Show kill distance in PvP deaths")]
public bool ShowKillDistance {
get;
set;
}
= true;
[JsonProperty(PropertyName = "High damage threshold for special kills")]
public float HighDamageThreshold {
get;
set;
}
= 75f;
[JsonProperty(PropertyName = "Use enhanced embeds for RCON messages")]
public bool UseEmbedForRcon {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Replacement string for tags")]
public string TagsReplacement {
get;
set;
}
= "`";
[JsonProperty(PropertyName = "Queue interval (1 message per ? seconds)")]
public float QueueInterval {
get;
set;
}
= 1f;
[JsonProperty(PropertyName = "Queue cooldown if connection error (seconds)")]
public float QueueCooldown {
get;
set;
}
= 60f;
[JsonProperty(PropertyName = "Public Chat Webhook URL")]
public string PublicChatWebhook {
get;
set;
}
= string.Empty;
[JsonProperty(PropertyName = "Private Admin Webhook URL")]
public string PrivateAdminWebhook {
get;
set;
}
= string.Empty;
[JsonProperty(PropertyName = "Server Messages Webhook URL")]
public string ServerMessagesWebhook {
get;
set;
}
= string.Empty;
[JsonProperty(PropertyName = "RCON command blacklist", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> RCONCommandBlacklist {
get;
set;
}
= new() {
"serverinfo", "server.hostname", "server.headerimage", "server.description", "server.url", "playerlist", "status"
}
;
[JsonProperty(PropertyName = "RCON trusted IPs (hide connections from these)", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> RCONTrustedIPs {
get;
set;
}
= new() {
"127.0.0.1", "::1"
}
;
[JsonProperty(PropertyName = "Steam Web API Key (for profile data)")]
public string SteamWebAPIKey {
get;
set;
}
= string.Empty;
}
private class EventSettings {
[JsonProperty(PropertyName = "Enabled?")]
public bool Enabled {
get;
set;
}
= false;
}
private class DeathSettings {
[JsonProperty(PropertyName = "Enabled?")]
public bool Enabled {
get;
set;
}
= false;
[JsonProperty(PropertyName = "Enable PvP deaths")]
public bool EnablePvP {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Enable PvE deaths")]
public bool EnablePvE {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Enable suicide deaths")]
public bool EnableSuicide {
get;
set;
}
= true;
[JsonProperty(PropertyName = "Enable drowning deaths")]
public bool EnableDrowning {
get;
set;
}
= true;
}
protected override void LoadConfig() {
base.LoadConfig();
try {
_configData = Config.ReadObject<ConfigData>();
if (_configData == null) {
LoadDefaultConfig();
}
ValidateConfig();
SaveConfig();
}
catch {
PrintError("The configuration file is corrupted");
LoadDefaultConfig();
ValidateConfig();
SaveConfig();
}
}
protected override void LoadDefaultConfig() {
PrintWarning("Creating a new configuration file");
_configData = new ConfigData();
}
protected override void SaveConfig() => Config.WriteObject(_configData);
private void ValidateConfig() {
bool needsSave = false;
// Validate Global Settings
if (_configData.GlobalSettings == null) {
_configData.GlobalSettings = new GlobalSettings();
PrintWarning("GlobalSettings was null, reset to default");
needsSave = true;
}
// Validate QueueInterval
if (_configData.GlobalSettings.QueueInterval <= 0) {
_configData.GlobalSettings.QueueInterval = 1f;
PrintWarning("QueueInterval was invalid, reset to 1.0");
needsSave = true;
}
// Validate QueueCooldown
if (_configData.GlobalSettings.QueueCooldown <= 0) {
_configData.GlobalSettings.QueueCooldown = 60f;
PrintWarning("QueueCooldown was invalid, reset to 60.0");
needsSave = true;
}
// Validate TagsReplacement
if (string.IsNullOrEmpty(_configData.GlobalSettings.TagsReplacement)) {
_configData.GlobalSettings.TagsReplacement = "`";
PrintWarning("TagsReplacement was empty, reset to '`'");
needsSave = true;
}
// Validate RCONCommandBlacklist
if (_configData.GlobalSettings.RCONCommandBlacklist == null) {
_configData.GlobalSettings.RCONCommandBlacklist = new List<string> {
"playerlist", "status"
}
;
PrintWarning("RCONCommandBlacklist was null, reset to default");
needsSave = true;
}
// Validate RCONTrustedIPs
if (_configData.GlobalSettings.RCONTrustedIPs == null) {
_configData.GlobalSettings.RCONTrustedIPs = new List<string> {
"127.0.0.1", "::1"
}
;
PrintWarning("RCONTrustedIPs was null, reset to default");
needsSave = true;
}
// Validate HighDamageThreshold
if (_configData.GlobalSettings.HighDamageThreshold <= 0) {
_configData.GlobalSettings.HighDamageThreshold = 75f;
PrintWarning("HighDamageThreshold was invalid, reset to 75.0");
needsSave = true;
}
// Validate Steam API Key
if (string.IsNullOrEmpty(_configData.GlobalSettings.SteamWebAPIKey)) {
// Silently skip Steam profile data if no API key configured
}
else if (_configData.GlobalSettings.SteamWebAPIKey.Length != 32) {
PrintWarning("Steam Web API Key appears to be invalid (should be 32 characters).Steam profile data may not work.");
}
// Validate Webhook URLs
if (!IsValidWebhookUrl(_configData.GlobalSettings.PublicChatWebhook) && !string.IsNullOrEmpty(_configData.GlobalSettings.PublicChatWebhook)) {
PrintWarning("PublicChatWebhook URL appears to be invalid");
}
if (!IsValidWebhookUrl(_configData.GlobalSettings.PrivateAdminWebhook) && !string.IsNullOrEmpty(_configData.GlobalSettings.PrivateAdminWebhook)) {
PrintWarning("PrivateAdminWebhook URL appears to be invalid");
}
if (!IsValidWebhookUrl(_configData.GlobalSettings.ServerMessagesWebhook) && !string.IsNullOrEmpty(_configData.GlobalSettings.ServerMessagesWebhook)) {
PrintWarning("ServerMessagesWebhook URL appears to be invalid");
}
// Validate ALL Event Settings - Force creation if missing
if (_configData.AirEventSettings == null) {
_configData.AirEventSettings = new EventSettings();
PrintWarning("AirEventSettings was null, created new instance");
needsSave = true;
}
if (_configData.AirfieldEventSettings == null) {
_configData.AirfieldEventSettings = new EventSettings();
PrintWarning("AirfieldEventSettings was null, created new instance");
needsSave = true;
}
if (_configData.ArcticBaseEventSettings == null) {
_configData.ArcticBaseEventSettings = new EventSettings();
PrintWarning("ArcticBaseEventSettings was null, created new instance");
needsSave = true;
}
if (_configData.ArmoredTrainEventSettings == null) {
_configData.ArmoredTrainEventSettings = new EventSettings();
PrintWarning("ArmoredTrainEventSettings was null, created new instance");
needsSave = true;
}
if (_configData.ChatSettings == null) {
_configData.ChatSettings = new EventSettings();
PrintWarning("ChatSettings was null, created new instance");
needsSave = true;
}
if (_configData.ChatTeamSettings == null) {
_configData.ChatTeamSettings = new EventSettings();
PrintWarning("ChatTeamSettings was null, created new instance");
needsSave = true;
}
if (_configData.ChristmasSettings == null) {
_configData.ChristmasSettings = new EventSettings();
PrintWarning("ChristmasSettings was null, created new instance");
needsSave = true;
}
if (_configData.EasterSettings == null) {
_configData.EasterSettings = new EventSettings();
PrintWarning("EasterSettings was null, created new instance");
needsSave = true;
}
if (_configData.GasStationEventSettings == null) {
_configData.GasStationEventSettings = new EventSettings();
PrintWarning("GasStationEventSettings was null, created new instance");
needsSave = true;
}
if (_configData.HalloweenSettings == null) {
_configData.HalloweenSettings = new EventSettings();
PrintWarning("HalloweenSettings was null, created new instance");
needsSave = true;
}
if (_configData.PermissionsSettings == null) {
_configData.PermissionsSettings = new EventSettings();
PrintWarning("PermissionsSettings was null, created new instance");
needsSave = true;
}
if (_configData.PlayerDeathSettings == null) {
_configData.PlayerDeathSettings = new DeathSettings();
PrintWarning("PlayerDeathSettings was null, created new instance");
needsSave = true;
}
if (_configData.PlayerConnectedInfoSettings == null) {
_configData.PlayerConnectedInfoSettings = new EventSettings();
PrintWarning("PlayerConnectedInfoSettings was null, created new instance");
needsSave = true;
}
if (_configData.PlayerDisconnectedSettings == null) {
_configData.PlayerDisconnectedSettings = new EventSettings();
PrintWarning("PlayerDisconnectedSettings was null, created new instance");
needsSave = true;
}
if (_configData.RaidableBasesSettings == null) {
_configData.RaidableBasesSettings = new EventSettings();
PrintWarning("RaidableBasesSettings was null, created new instance");
needsSave = true;
}
if (_configData.RconCommandSettings == null) {
_configData.RconCommandSettings = new EventSettings();
PrintWarning("RconCommandSettings was null, created new instance");
needsSave = true;
}
if (_configData.RconConnectionSettings == null) {
_configData.RconConnectionSettings = new EventSettings();
PrintWarning("RconConnectionSettings was null, created new instance");
needsSave = true;
}
if (_configData.SantaSleighSettings == null) {
_configData.SantaSleighSettings = new EventSettings();
PrintWarning("SantaSleighSettings was null, created new instance");
needsSave = true;
}
if (_configData.ServerMessagesSettings == null) {
_configData.ServerMessagesSettings = new EventSettings();
PrintWarning("ServerMessagesSettings was null, created new instance");
needsSave = true;
}
if (_configData.ServerStateSettings == null) {
_configData.ServerStateSettings = new EventSettings();
PrintWarning("ServerStateSettings was null, created new instance");
needsSave = true;
}
if (_configData.SputnikEventSettings == null) {
_configData.SputnikEventSettings = new EventSettings();
PrintWarning("SputnikEventSettings was null, created new instance");
needsSave = true;
}
if (_configData.SupermarketEventSettings == null) {
_configData.SupermarketEventSettings = new EventSettings();
PrintWarning("SupermarketEventSettings was null, created new instance");
needsSave = true;
}
if (_configData.UserBannedSettings == null) {
_configData.UserBannedSettings = new EventSettings();
PrintWarning("UserBannedSettings was null, created new instance");
needsSave = true;
}
if (_configData.UserKickedSettings == null) {
_configData.UserKickedSettings = new EventSettings();
PrintWarning("UserKickedSettings was null, created new instance");
needsSave = true;
}
if (_configData.UserMutedSettings == null) {
_configData.UserMutedSettings = new EventSettings();
PrintWarning("UserMutedSettings was null, created new instance");
needsSave = true;
}
if (_configData.UserNameUpdateSettings == null) {
_configData.UserNameUpdateSettings = new EventSettings();
PrintWarning("UserNameUpdateSettings was null, created new instance");
needsSave = true;
}
if (needsSave) {
PrintWarning("Configuration validation completed - saving updated config");
}
}
private bool IsValidWebhookUrl(string url) {
if (string.IsNullOrEmpty(url)) return false;
return url.StartsWith("https://discord.com/api/webhooks/") || url.StartsWith("https://discordapp.com/api/webhooks/");
}
#endregion Configuration
#region Localization
public string Lang(string key, string userIDString = null, params object[] args) {
try {
return string.Format(lang.GetMessage(key, this, userIDString).Replace("{time}", $"<t:{DateTimeOffset.Now.ToUnixTimeSeconds()}:t>"), args);
}
catch (Exception ex) {
PrintError($"Lang Key '{key}' threw exception:\n{ex}");
throw;
}
}
private static class LangKeys {
public static class Event {
private const string Base = nameof(Event) + ".";
public const string AirEvent = Base + nameof(AirEvent);
public const string AirfieldEvent = Base + nameof(AirfieldEvent);
public const string ArcticBaseEvent = Base + nameof(ArcticBaseEvent);
public const string ArmoredTrainEvent = Base + nameof(ArmoredTrainEvent);
public const string Chat = Base + nameof(Chat);
public const string ChatTeam = Base + nameof(ChatTeam);
public const string Christmas = Base + nameof(Christmas);
public const string Death = Base + nameof(Death);
public const string Easter = Base + nameof(Easter);
public const string EasterWinner = Base + nameof(EasterWinner);
public const string GasStationEvent = Base + nameof(GasStationEvent);
public const string Halloween = Base + nameof(Halloween);
public const string HalloweenWinner = Base + nameof(HalloweenWinner);
public const string Initialized = Base + nameof(Initialized);
public const string PlayerConnected = Base + nameof(PlayerConnected);
public const string PlayerConnectedInfo = Base + nameof(PlayerConnectedInfo);
public const string PlayerDisconnected = Base + nameof(PlayerDisconnected);
public const string RconCommand = Base + nameof(RconCommand);
public const string RconConnection = Base + nameof(RconConnection);
public const string SantaSleigh = Base + nameof(SantaSleigh);
public const string ServerMessage = Base + nameof(ServerMessage);
public const string Shutdown = Base + nameof(Shutdown);
public const string SputnikEvent = Base + nameof(SputnikEvent);
public const string SupermarketEvent = Base + nameof(SupermarketEvent);
public const string UserBanned = Base + nameof(UserBanned);
public const string UserKicked = Base + nameof(UserKicked);
public const string UserMuted = Base + nameof(UserMuted);
public const string UserNameUpdated = Base + nameof(UserNameUpdated);
public const string UserUnbanned = Base + nameof(UserUnbanned);
public const string UserUnmuted = Base + nameof(UserUnmuted);
}
public static class Permission {
private const string Base = nameof(Permission) + ".";
public const string GroupCreated = Base + nameof(GroupCreated);
public const string GroupDeleted = Base + nameof(GroupDeleted);
public const string UserGroupAdded = Base + nameof(UserGroupAdded);
public const string UserGroupRemoved = Base + nameof(UserGroupRemoved);
public const string UserPermissionGranted = Base + nameof(UserPermissionGranted);
public const string UserPermissionRevoked = Base + nameof(UserPermissionRevoked);
}
public static class Plugin {
private const string Base = nameof(Plugin) + ".";
public const string RaidableBaseCompleted = Base + nameof(RaidableBaseCompleted);
public const string RaidableBaseEnded = Base + nameof(RaidableBaseEnded);
public const string RaidableBaseStarted = Base + nameof(RaidableBaseStarted);
public const string TimedGroupAdded = Base + nameof(TimedGroupAdded);
public const string TimedGroupExtended = Base + nameof(TimedGroupExtended);
public const string TimedPermissionExtended = Base + nameof(TimedPermissionExtended);
public const string TimedPermissionGranted = Base + nameof(TimedPermissionGranted);
}
public static class Format {
private const string Base = nameof(Format) + ".";
public const string Day = Base + nameof(Day);
public const string Days = Base + nameof(Days);
public const string Easy = Base + nameof(Easy);
public const string Expert = Base + nameof(Expert);
public const string Hard = Base + nameof(Hard);
public const string Hour = Base + nameof(Hour);
public const string Hours = Base + nameof(Hours);
public const string Medium = Base + nameof(Medium);
public const string Minute = Base + nameof(Minute);
public const string Minutes = Base + nameof(Minutes);
public const string Nightmare = Base + nameof(Nightmare);
public const string Second = Base + nameof(Second);
public const string Seconds = Base + nameof(Seconds);
}
}
protected override void LoadDefaultMessages() {
lang.RegisterMessages(new Dictionary<string, string> {
[LangKeys.Event.AirEvent]
= ":helicopter: {time} Air Event started", [LangKeys.Event.AirfieldEvent] = ":airplane: {time} Airfield Event started", [LangKeys.Event.ArcticBaseEvent] = ":snowflake: {time} Arctic Base Event started", [LangKeys.Event.ArmoredTrainEvent] = ":train2: {time} Armored Train Event started", [LangKeys.Event.Chat] = ":speech_left: {time} **{0}**: {1}", [LangKeys.Event.ChatTeam] = ":busts_in_silhouette: {time} **{0}**: {1}", [LangKeys.Event.Christmas] = ":christmas_tree: {time} Christmas event started", [LangKeys.Event.Death] = ":skull: {time} {0}", [LangKeys.Event.Easter] = ":egg: {time} Easter event started", [LangKeys.Event.EasterWinner] = ":egg: {time} Easter event ended.The winner is `{0}`", [LangKeys.Event.GasStationEvent] = ":fuelpump: {time} Gas Station Event started", [LangKeys.Event.Halloween] = ":jack_o_lantern: {time} Halloween event started", [LangKeys.Event.HalloweenWinner] = ":jack_o_lantern: {time} Halloween event ended.The winner is `{0}`", [LangKeys.Event.Initialized] = ":ballot_box_with_check: {time} Server is online again!", [LangKeys.Event.PlayerConnectedInfo] = ":detective: {time} {0} connected.SteamID: `{1}` IP: `{2}`", [LangKeys.Event.PlayerDisconnected] = ":x: {time} {0} disconnected ({1}) SteamID: `{2}` IP: `{3}`", [LangKeys.Event.RconCommand] = ":satellite: {time} RCON command `{0}` is run from `{1}`", [LangKeys.Event.RconConnection] = ":satellite: {time} RCON connection is opened from `{0}`", [LangKeys.Event.SantaSleigh] = ":santa: {time} SantaSleigh Event started", [LangKeys.Event.ServerMessage] = ":desktop: {time} `{0}`", [LangKeys.Event.Shutdown] = ":stop_sign: {time} Server is shutting down!", [LangKeys.Event.SputnikEvent] = ":satellite_orbital: {time} Sputnik Event started", [LangKeys.Event.SupermarketEvent] = ":convenience_store: {time} Supermarket Event started", [LangKeys.Event.UserBanned] = ":no_entry: {time} Player `{0}` SteamID: `{1}` IP: `{2}` was banned: `{3}`", [LangKeys.Event.UserKicked] = ":hiking_boot: {time} Player `{0}` SteamID: `{1}` was kicked: `{2}`", [LangKeys.Event.UserMuted] = ":mute: {time} `{0}` was muted by `{1}` for `{2}` (`{3}`)", [LangKeys.Event.UserNameUpdated] = ":label: {time} `{0}` changed name to `{1}` SteamID: `{2}`", [LangKeys.Event.UserUnbanned] = ":ok: {time} Player `{0}` SteamID: `{1}` IP: `{2}` was unbanned", [LangKeys.Event.UserUnmuted] = ":speaker: {time} `{0}` was unmuted `{1}`", [LangKeys.Format.Day] = "day", [LangKeys.Format.Days] = "days", [LangKeys.Format.Easy] = "Easy", [LangKeys.Format.Expert] = "Expert", [LangKeys.Format.Hard] = "Hard", [LangKeys.Format.Hour] = "hour", [LangKeys.Format.Hours] = "hours", [LangKeys.Format.Medium] = "Medium", [LangKeys.Format.Minute] = "minute", [LangKeys.Format.Minutes] = "minutes", [LangKeys.Format.Nightmare] = "Nightmare", [LangKeys.Format.Second] = "second", [LangKeys.Format.Seconds] = "seconds", [LangKeys.Permission.GroupCreated] = ":family: {time} Group `{0}` has been created", [LangKeys.Permission.GroupDeleted] = ":family: {time} Group `{0}` has been deleted", [LangKeys.Permission.UserGroupAdded] = ":family: {time} `{0}` `{1}` is added to group `{2}`", [LangKeys.Permission.UserGroupRemoved] = ":family: {time} `{0}` `{1}` is removed from group `{2}`", [LangKeys.Permission.UserPermissionGranted] = ":key: {time} `{0}` `{1}` is granted `{2}`", [LangKeys.Permission.UserPermissionRevoked] = ":key: {time} `{0}` `{1}` is revoked `{2}`", [LangKeys.Plugin.RaidableBaseCompleted] = ":homes: {time} {1} Raidable Base owned by {2} at `{0}` has been raided by **{3}**", [LangKeys.Plugin.RaidableBaseEnded] = ":homes: {time} {1} Raidable Base at `{0}` has ended", [LangKeys.Plugin.RaidableBaseStarted] = ":homes: {time} {1} Raidable Base spawned at `{0}`", [LangKeys.Plugin.TimedGroupAdded] = ":timer: {time} `{0}` `{1}` is added to `{2}` for {3}", [LangKeys.Plugin.TimedGroupExtended] = ":timer: {time} `{0}` `{1}` timed group `{2}` is extended to {3}", [LangKeys.Plugin.TimedPermissionExtended] = ":timer: {time} `{0}` `{1}` timed permission `{2}` is extended to {3}", [LangKeys.Plugin.TimedPermissionGranted] = ":timer: {time} `{0}` `{1}` is granted `{2}` for {3}",
}
, this);
}
#endregion Localization
#region Events Hooks
private void OnBetterChatMuted(IPlayer target, IPlayer initiator, string reason) {
LogToConsole($"{target.Name} was muted by {initiator.Name} for ever ({reason})");
DiscordSendMessage(Lang(LangKeys.Event.UserMuted, null, ReplaceChars(target.Name), ReplaceChars(initiator.Name), "ever", ReplaceChars(reason)), _configData.GlobalSettings.PrivateAdminWebhook);
}
private void OnBetterChatMuteExpired(IPlayer player) {
LogToConsole($"{player.Name} was unmuted by SERVER");
DiscordSendMessage(Lang(LangKeys.Event.UserUnmuted, null, ReplaceChars(player.Name), "SERVER"), _configData.GlobalSettings.PrivateAdminWebhook);
}
private void OnBetterChatTimeMuted(IPlayer target, IPlayer initiator, TimeSpan time, string reason) {
LogToConsole($"{target.Name} was muted by {initiator.Name} for {time.ToShortString()} ({reason})");
DiscordSendMessage(Lang(LangKeys.Event.UserMuted, null, ReplaceChars(target.Name), ReplaceChars(initiator.Name), time.ToShortString(), ReplaceChars(reason)), _configData.GlobalSettings.PrivateAdminWebhook);
}
private void OnBetterChatUnmuted(IPlayer target, IPlayer initiator) {
LogToConsole($"{target.Name} was unmuted by {initiator.Name}");
DiscordSendMessage(Lang(LangKeys.Event.UserUnmuted, null, ReplaceChars(target.Name), ReplaceChars(initiator.Name)), _configData.GlobalSettings.PrivateAdminWebhook);
}
private void OnEntityDeath(BasePlayer player, HitInfo info) {
if (!_configData.PlayerDeathSettings.Enabled || !player.IsValid()) {
return;
}
if (_configData.GlobalSettings.HideNPC && (player.IsNpc || !player.userID.IsSteamId())) {
return;
}
// Check if this death type should be reported
string deathType = GetDeathType(player, info);
if (!ShouldReportDeathType(deathType)) {
return;
}
// Capture death position
Vector3 deathPosition = player.transform.position;
// Send enhanced death message with embeds if enabled
if (_configData.GlobalSettings.UseEmbedForDeaths) {
SendEnhancedDeathMessage(player, info, deathPosition);
}
else {
// Fallback to simple text message
string deathMessage = GetSimpleDeathMessage(player, info, deathPosition);
string discordMessage = $":skull: <t:{DateTimeOffset.Now.ToUnixTimeSeconds()}:t> {deathMessage}";
DiscordSendMessage(discordMessage, _configData.GlobalSettings.PrivateAdminWebhook);
}
}
private void SendEnhancedDeathMessage(BasePlayer victim, HitInfo info, Vector3 deathPosition) {
string victimName = ReplaceChars(victim.displayName);
string deathType = GetDeathType(victim, info);
string deathIcon = GetDeathIcon(victim, info);
int embedColor = GetDeathColor(victim, info);
var embed = new DiscordEmbed().SetColor(embedColor).SetTimestamp(DateTimeOffset.Now);
string description = "";
string weapon = GetWeaponName(info);
// Check for drowning first
if (IsDrowningDeath(info)) {
description = $"**{victimName}** drowned";
embed.SetTitle($"🌊 Drowning").AddField("Death Type", "Drowning", true);
}
// Build death description based on type
else if (info?.Initiator == null) {
description = $"**{victimName}** died";
embed.SetTitle($"{deathIcon} Player Death").AddField("Death Type", "Unknown", true);
}
else if (info.Initiator is BasePlayer killer) {
if (killer.userID == victim.userID) {
description = $"**{victimName}** committed suicide";
embed.SetTitle($"{deathIcon} Suicide").AddField("Death Type", "Suicide", true);
if (!string.IsNullOrEmpty(weapon)) {
embed.AddField("Method", weapon, true);
}
}
else {
// PvP Kill
float damage = info.damageTypes.Total();
float distance = Vector3.Distance(victim.transform.position, killer.transform.position);
bool isHighDamage = damage >= _configData.GlobalSettings.HighDamageThreshold;
bool isHeadshot = IsHeadshot(info);
string killerName = ReplaceChars(killer.displayName);
description = $"**{victimName}** was eliminated by **{killerName}**";
// Enhanced title based on kill type - prioritize headshot
string title;
if (isHeadshot) {
title = "🎯💀 Devastating Headshot!";
}
else if (isHighDamage) {
title = "🎯 Devastating Kill!";
}
else {
title = "⚔️ Player Eliminated";
}
embed.SetTitle(title).SetColor(embedColor).AddField("💀 Elimination", $"**Killer:** {killerName}\n**Victim:** {victimName}", false);
// Combat details
if (_configData.GlobalSettings.ShowCombatDetails) {
string combatDetails = GetCombatDetails(info, distance, damage, weapon, isHighDamage);
embed.AddField("🔫 Combat Stats", combatDetails, false);
}
}
}
else {
// PvE Death
string entityName = GetEntityName(info.Initiator);
description = $"**{victimName}** was killed by **{entityName}**";
embed.SetTitle($"{deathIcon} Environmental Death").AddField("Death Type", "PvE", true).AddField("Killed By", entityName, true);
if (!string.IsNullOrEmpty(weapon)) {
embed.AddField("Method", weapon, true);
}
}
embed.SetDescription(description);
// Add coordinates if enabled
if (_configData.GlobalSettings.IncludeDeathCoordinates) {
string gridPos = GetGridPosition(deathPosition);
string coords = $"**Grid:** {gridPos}\n**Position:** {deathPosition.x:F1}, {deathPosition.y:F1}, {deathPosition.z:F1}";
embed.AddField("📍 Death Location", coords, false);
embed.AddField("🚁 Quick Teleport", $"`teleportpos {deathPosition.x:F1} {deathPosition.y:F1} {deathPosition.z:F1}`", false);
}
// Add separator
embed.AddField("─────────────────────", "", false);
var discordMessage = new DiscordMessage().AddEmbed(embed);
DiscordSendEmbedMessage(discordMessage, _configData.GlobalSettings.PrivateAdminWebhook);
}
private string GetCombatDetails(HitInfo info, float distance, float damage, string weapon, bool isHighDamage) {
string distanceCategory = GetDistanceCategory(distance);
string bodyPart = GetBodyPartHit(info);
bool isHeadshot = IsHeadshot(info);
// Enhanced damage display with body part
string damageInfo;
if (isHeadshot) {
damageInfo = $"**{damage:F1}** 💀 ({bodyPart} shot)";
}
else if (isHighDamage) {
damageInfo = $"**{damage:F1}** 🎯 ({bodyPart} shot)";
}
else {
damageInfo = $"{damage:F1} ({bodyPart} shot)";
}
string details = $"**Weapon:** {weapon ?? "Unknown"}\n**Damage:** {damageInfo}";
if (_configData.GlobalSettings.ShowKillDistance) {
details += $"\n**Range:** {distance:F1}m ({distanceCategory})";
}
// Add special indicators
if (isHeadshot) {
details += "\n💀 **HEADSHOT ELIMINATION!**";
}
else if (isHighDamage) {
details += "\n🎯 **High Damage Hit!**";
}
if (distance > 150f) {
details += "\n🏹 **Long Range Snipe!**";
}
else if (distance < 3f) {
details += "\n💥 **Point Blank!**";
}
return details;
}
private string GetDistanceCategory(float distance) {
return dis
// ... [Code preview truncated due to length. Full source in attached file] ...