C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using UnityEngine;
using System;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Oxide.Plugins
{
[Info("rPop", "Ftuoil Xelrash", "1.0.1")]
[Description("Displays server population statistics and sends performance updates to Discord")]
public class rPop : RustPlugin
{
#region Configuration
private ConfigData config;
private PluginData pluginData;
private DateTime lastPopCommandTime = DateTime.MinValue;
private Timer performanceTimer;
private Timer inGameMessageTimer;
private readonly Dictionary<string, DateTime> lastDiscordMessage = new Dictionary<string, DateTime>();
private SaveInfo _saveInfo;
private bool _isOnline = false; // Server status tracking
public class ConfigData
{
[JsonProperty("Settings")] public PluginSettings Settings = new PluginSettings();
}
public class PluginSettings
{
[JsonProperty("Enable !pop Command")] public bool EnablePopCommand = true;
[JsonProperty("Command Cooldown (minutes)")] public float CommandCooldown = 5f;
[JsonProperty("Show Last Wipe Date")] public bool ShowLastWipeDate = true;
[JsonProperty("Show Last Blueprint Wipe Date")] public bool ShowLastBlueprintWipeDate = true;
[JsonProperty("Show Next Wipe Date")] public bool ShowNextWipeDate = true;
[JsonProperty("Show Network IO")] public bool ShowNetworkIO = true;
[JsonProperty("Show Protocol")] public bool ShowProtocol = true;
[JsonProperty("Show Server Status")] public bool ShowServerStatus = true;
[JsonProperty("Show Players Joining")] public bool ShowPlayersJoining = true;
[JsonProperty("Show Players Sleeping")] public bool ShowPlayersSleeping = true;
[JsonProperty("Show Admins Online")] public bool ShowAdminsOnline = true;
[JsonProperty("Hide Zero Values")] public bool HideZeroValues = true;
[JsonProperty("Discord Webhook URL")] public string WebhookURL = "";
[JsonProperty("Discord Rate Limit (seconds)")] public float DiscordRateLimit = 1f;
[JsonProperty("Enable Discord Performance Messages")] public bool EnablePerformanceMessages = true;
[JsonProperty("Performance Message Interval (minutes)")] public float PerformanceMessageInterval = 3f;
[JsonProperty("Enable In-Game Performance Messages")] public bool EnableInGamePerformanceMessages = true;
[JsonProperty("In-Game Message Interval (minutes)")] public float InGameMessageInterval = 60f;
[JsonProperty("Use Server Header Image")] public bool UseServerHeaderImage = true;
[JsonProperty("Fallback Discord Image URL")] public string FallbackImageURL = "https://files.facepunch.com/lewis/1b2911b1/rust-logo.png";
[JsonProperty("Use Thumbnail Instead of Image")] public bool UseThumbnail = true;
[JsonProperty("Show Discord Image")] public bool ShowDiscordImage = true;
[JsonProperty("Discord Bot Name")] public string DiscordBotName = "Live Server Statistics";
[JsonProperty("Show Population Records")] public bool ShowPopulationRecords = true;
[JsonProperty("Show Total Players Ever")] public bool ShowTotalPlayersEver = true;
[JsonProperty("Show Average Connection Time")] public bool ShowAverageConnectionTime = true;
[JsonProperty("Enable Instant Population Updates")] public bool EnableInstantPopulationUpdates = true;
[JsonProperty("Population Update Delay (seconds)")] public float PopulationUpdateDelay = 2f;
}
public class PluginData
{
[JsonProperty("Today High Population")] public PopulationRecord TodayHigh = new PopulationRecord();
[JsonProperty("Monthly High Population")] public PopulationRecord MonthlyHigh = new PopulationRecord();
[JsonProperty("All Time High Population")] public PopulationRecord AllTimeHigh = new PopulationRecord();
[JsonProperty("Last Reset Date")] public DateTime LastResetDate = DateTime.Today;
[JsonProperty("Last Monthly Reset")] public DateTime LastMonthlyReset = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
[JsonProperty("Discord Status Message ID")] public string StatusMessageId = null;
}
public class PopulationRecord
{
[JsonProperty("Count")] public int Count = 0;
[JsonProperty("Date")] public DateTime Date = DateTime.MinValue;
}
protected override void LoadDefaultConfig()
{
config = new ConfigData();
SaveConfig();
Puts("Default configuration created.");
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<ConfigData>();
if (config == null)
{
LoadDefaultConfig();
return;
}
ValidateConfiguration();
}
catch (Exception ex)
{
PrintError($"Error loading configuration: {ex.Message}");
LoadDefaultConfig();
}
}
private void ValidateConfiguration()
{
bool needsSave = false;
if (config.Settings.PerformanceMessageInterval < 1f)
{
Puts($"Performance message interval ({config.Settings.PerformanceMessageInterval}) is too low, setting to minimum of 1 minute.");
config.Settings.PerformanceMessageInterval = 1f;
needsSave = true;
}
if (config.Settings.DiscordRateLimit < 0.1f)
{
Puts($"Discord rate limit ({config.Settings.DiscordRateLimit}) is too low, setting to minimum of 0.1 seconds.");
config.Settings.DiscordRateLimit = 0.1f;
needsSave = true;
}
if (config.Settings.PopulationUpdateDelay < 1f)
{
Puts($"Population update delay ({config.Settings.PopulationUpdateDelay}) is too low, setting to minimum of 1 second.");
config.Settings.PopulationUpdateDelay = 1f;
needsSave = true;
}
if (!ConfigHasAllProperties())
{
Puts("Adding missing configuration properties...");
needsSave = true;
}
if (needsSave)
{
SaveConfig();
Puts("Configuration updated with missing properties (existing settings preserved).");
}
}
private bool ConfigHasAllProperties()
{
try
{
var configText = Config.ReadObject<Dictionary<string, object>>();
var settings = configText.GetValueOrDefault("Settings") as Dictionary<string, object>;
if (settings == null) return false;
bool hasAll = settings.ContainsKey("Discord Webhook URL") &&
settings.ContainsKey("Discord Rate Limit (seconds)") &&
settings.ContainsKey("Enable Discord Performance Messages") &&
settings.ContainsKey("Performance Message Interval (minutes)") &&
settings.ContainsKey("Enable In-Game Performance Messages") &&
settings.ContainsKey("In-Game Message Interval (minutes)") &&
settings.ContainsKey("Use Server Header Image") &&
settings.ContainsKey("Fallback Discord Image URL") &&
settings.ContainsKey("Use Thumbnail Instead of Image") &&
settings.ContainsKey("Show Discord Image") &&
settings.ContainsKey("Discord Bot Name") &&
settings.ContainsKey("Show Players Joining") &&
settings.ContainsKey("Show Players Sleeping") &&
settings.ContainsKey("Show Admins Online") &&
settings.ContainsKey("Hide Zero Values") &&
settings.ContainsKey("Show Population Records") &&
settings.ContainsKey("Show Total Players Ever") &&
settings.ContainsKey("Show Average Connection Time") &&
settings.ContainsKey("Show Last Blueprint Wipe Date") &&
settings.ContainsKey("Show Next Wipe Date") &&
settings.ContainsKey("Show Network IO") &&
settings.ContainsKey("Show Protocol") &&
settings.ContainsKey("Show Server Status") &&
settings.ContainsKey("Enable Instant Population Updates") &&
settings.ContainsKey("Population Update Delay (seconds)");
if (!hasAll)
{
if (!settings.ContainsKey("Discord Webhook URL"))
config.Settings.WebhookURL = "";
if (!settings.ContainsKey("Discord Rate Limit (seconds)"))
config.Settings.DiscordRateLimit = 1f;
if (!settings.ContainsKey("Enable Discord Performance Messages"))
config.Settings.EnablePerformanceMessages = true;
if (!settings.ContainsKey("Performance Message Interval (minutes)"))
config.Settings.PerformanceMessageInterval = 3f;
if (!settings.ContainsKey("Enable In-Game Performance Messages"))
config.Settings.EnableInGamePerformanceMessages = true;
if (!settings.ContainsKey("In-Game Message Interval (minutes)"))
config.Settings.InGameMessageInterval = 60f;
if (!settings.ContainsKey("Use Server Header Image"))
config.Settings.UseServerHeaderImage = true;
if (!settings.ContainsKey("Fallback Discord Image URL"))
config.Settings.FallbackImageURL = "https://files.facepunch.com/lewis/1b2911b1/rust-logo.png";
if (!settings.ContainsKey("Use Thumbnail Instead of Image"))
config.Settings.UseThumbnail = true;
if (!settings.ContainsKey("Show Discord Image"))
config.Settings.ShowDiscordImage = true;
if (!settings.ContainsKey("Discord Bot Name"))
config.Settings.DiscordBotName = "Live Server Statistics";
if (!settings.ContainsKey("Show Players Joining"))
config.Settings.ShowPlayersJoining = true;
if (!settings.ContainsKey("Show Players Sleeping"))
config.Settings.ShowPlayersSleeping = true;
if (!settings.ContainsKey("Show Admins Online"))
config.Settings.ShowAdminsOnline = true;
if (!settings.ContainsKey("Hide Zero Values"))
config.Settings.HideZeroValues = true;
if (!settings.ContainsKey("Show Population Records"))
config.Settings.ShowPopulationRecords = true;
if (!settings.ContainsKey("Show Total Players Ever"))
config.Settings.ShowTotalPlayersEver = true;
if (!settings.ContainsKey("Show Average Connection Time"))
config.Settings.ShowAverageConnectionTime = true;
if (!settings.ContainsKey("Show Last Blueprint Wipe Date"))
config.Settings.ShowLastBlueprintWipeDate = true;
if (!settings.ContainsKey("Show Next Wipe Date"))
config.Settings.ShowNextWipeDate = true;
if (!settings.ContainsKey("Show Network IO"))
config.Settings.ShowNetworkIO = true;
if (!settings.ContainsKey("Show Protocol"))
config.Settings.ShowProtocol = true;
if (!settings.ContainsKey("Show Server Status"))
config.Settings.ShowServerStatus = true;
if (!settings.ContainsKey("Enable Instant Population Updates"))
config.Settings.EnableInstantPopulationUpdates = true;
if (!settings.ContainsKey("Population Update Delay (seconds)"))
config.Settings.PopulationUpdateDelay = 2f;
}
return hasAll;
}
catch
{
return false;
}
}
protected override void SaveConfig()
{
try
{
Config.WriteObject(config, true);
}
catch (Exception ex)
{
PrintError($"Error saving configuration: {ex.Message}");
}
}
#endregion
#region Data Management
private void LoadData()
{
try
{
pluginData = Interface.Oxide.DataFileSystem.ReadObject<PluginData>("rPop");
if (pluginData == null)
{
pluginData = new PluginData();
SaveData();
}
// Check for daily reset
if (pluginData.LastResetDate.Date < DateTime.Today)
{
pluginData.TodayHigh = new PopulationRecord();
pluginData.LastResetDate = DateTime.Today;
SaveData();
Puts("Daily population stats reset for new day.");
}
// Check for monthly reset
DateTime currentMonthStart = new DateTime(DateTime.Now.Year, DateTime.Now.Month, 1);
if (pluginData.LastMonthlyReset < currentMonthStart)
{
pluginData.MonthlyHigh = new PopulationRecord();
pluginData.LastMonthlyReset = currentMonthStart;
SaveData();
Puts("Monthly population stats reset for new month.");
}
Puts("Plugin data loaded successfully.");
}
catch (Exception ex)
{
PrintError($"Error loading data: {ex.Message}");
pluginData = new PluginData();
SaveData();
}
}
private void SaveData()
{
try
{
Interface.Oxide.DataFileSystem.WriteObject("rPop", pluginData);
}
catch (Exception ex)
{
PrintError($"Error saving data: {ex.Message}");
}
}
private void UpdatePopulationRecords()
{
try
{
int currentPopulation = BasePlayer.activePlayerList.Count;
DateTime now = DateTime.Now;
if (currentPopulation > pluginData.TodayHigh.Count)
{
pluginData.TodayHigh.Count = currentPopulation;
pluginData.TodayHigh.Date = now;
}
if (currentPopulation > pluginData.MonthlyHigh.Count)
{
pluginData.MonthlyHigh.Count = currentPopulation;
pluginData.MonthlyHigh.Date = now;
}
if (currentPopulation > pluginData.AllTimeHigh.Count)
{
pluginData.AllTimeHigh.Count = currentPopulation;
pluginData.AllTimeHigh.Date = now;
}
SaveData();
}
catch (Exception ex)
{
PrintError($"Error updating population records: {ex.Message}");
}
}
private double GetSessionPlayTime(BasePlayer player)
{
if (player?.net?.connection == null) return 0;
return player.net.connection.GetSecondsConnected();
}
private string GetAverageConnectionTime()
{
try
{
var activePlayers = BasePlayer.activePlayerList;
if (activePlayers.Count == 0) return "No players online";
double totalSeconds = 0;
int validPlayerCount = 0;
foreach (var player in activePlayers)
{
if (player?.net?.connection == null) continue;
double sessionTime = player.net.connection.GetSecondsConnected();
if (sessionTime > 0)
{
totalSeconds += sessionTime;
validPlayerCount++;
}
}
if (validPlayerCount == 0) return "No valid session data";
double averageSeconds = totalSeconds / validPlayerCount;
TimeSpan timeSpan = TimeSpan.FromSeconds(averageSeconds);
if (timeSpan.TotalHours >= 1)
return $"{(int)timeSpan.TotalHours:D2}:{timeSpan.Minutes:D2}:{timeSpan.Seconds:D2}";
else if (timeSpan.TotalMinutes >= 1)
return $"{timeSpan.Minutes}:{timeSpan.Seconds:D2} min";
else
return $"{timeSpan.Seconds} sec";
}
catch (Exception ex)
{
PrintError($"Error calculating average connection time: {ex.Message}");
return "Error calculating";
}
}
private int GetTotalPlayersEver()
{
try
{
string userdataPath = "userdata";
if (!Directory.Exists(userdataPath))
{
return 0;
}
var playerFolders = Directory.GetDirectories(userdataPath);
int validPlayerCount = 0;
foreach (string playerFolder in playerFolders)
{
string steamId = Path.GetFileName(playerFolder);
if (IsValidSteamID(steamId))
{
validPlayerCount++;
}
}
return validPlayerCount;
}
catch (Exception ex)
{
PrintError($"Error counting total players: {ex.Message}");
return 0;
}
}
private bool IsValidSteamID(string steamId)
{
return !string.IsNullOrEmpty(steamId) &&
steamId.Length >= 7 &&
steamId.All(char.IsDigit);
}
#endregion
#region Server Status Functions
private string GetServerStatus()
{
return _isOnline ? "✅ Online" : "❌ Offline";
}
private string GetServerStatusEmoji()
{
return _isOnline ? "✅" : "❌";
}
private string GetServerStatusText()
{
return _isOnline ? "Online" : "Offline";
}
#endregion
#region Network, Blueprint, and Protocol Functions
private string GetNetworkIO()
{
try
{
float networkIn = Network.Net.sv.GetStat(null, Network.BaseNetwork.StatTypeLong.BytesReceived_LastSecond) / 1024f;
float networkOut = Network.Net.sv.GetStat(null, Network.BaseNetwork.StatTypeLong.BytesSent_LastSecond) / 1024f;
return $"In: {networkIn:0.00} KB/s Out: {networkOut:0.00} KB/s";
}
catch (Exception ex)
{
PrintError($"Error getting network IO: {ex.Message}");
return "Network IO: Error";
}
}
private string GetServerProtocol()
{
try
{
return $"{Rust.Protocol.network}.{Rust.Protocol.save}.{Rust.Protocol.report}";
}
catch (Exception ex)
{
PrintError($"Error getting server protocol: {ex.Message}");
return "Unknown";
}
}
private DateTime GetLastBlueprintWipe()
{
try
{
if (_saveInfo == null)
{
string saveInfoPath = Path.Combine(World.SaveFolderName, $"player.blueprints.{Rust.Protocol.persistance}.db");
_saveInfo = SaveInfo.Create(saveInfoPath);
}
return _saveInfo?.CreationTime ?? DateTime.MinValue;
}
catch (Exception ex)
{
PrintError($"Error getting last blueprint wipe: {ex.Message}");
return DateTime.MinValue;
}
}
private string GetFormattedBlueprintWipeDate()
{
try
{
DateTime blueprintWipe = GetLastBlueprintWipe();
if (blueprintWipe == DateTime.MinValue)
{
return "Unknown";
}
TimeSpan timeSinceWipe = DateTime.Now - blueprintWipe;
if (timeSinceWipe.TotalDays < 1)
{
return $"{blueprintWipe:MMM dd, yyyy} ({(int)timeSinceWipe.TotalHours}h ago)";
}
else if (timeSinceWipe.TotalDays < 7)
{
return $"{blueprintWipe:MMM dd, yyyy} ({(int)timeSinceWipe.TotalDays}d ago)";
}
else
{
return $"{blueprintWipe:MMM dd, yyyy} ({(int)timeSinceWipe.TotalDays}d ago)";
}
}
catch (Exception ex)
{
PrintError($"Error formatting blueprint wipe date: {ex.Message}");
return "Unknown";
}
}
#endregion
#region Next Wipe Date Functions
private string GetServerTimeZoneAbbreviation()
{
try
{
TimeZoneInfo tz = TimeZoneInfo.Local;
var timezoneMap = new Dictionary<string, string>
{
{"Central Standard Time", "CST"},
{"Central Daylight Time", "CDT"},
{"Eastern Standard Time", "EST"},
{"Eastern Daylight Time", "EDT"},
{"Mountain Standard Time", "MST"},
{"Mountain Daylight Time", "MDT"},
{"Pacific Standard Time", "PST"},
{"Pacific Daylight Time", "PDT"},
{"UTC", "UTC"},
{"GMT Standard Time", "GMT"}
};
string currentName = tz.IsDaylightSavingTime(DateTime.Now) ? tz.DaylightName : tz.StandardName;
return timezoneMap.ContainsKey(currentName) ? timezoneMap[currentName] : "Server Time";
}
catch
{
return "Server Time";
}
}
private DateTime GetFirstThursdayOfMonth(int year, int month)
{
DateTime firstOfMonth = new DateTime(year, month, 1);
int daysUntilThursday = ((int)DayOfWeek.Thursday - (int)firstOfMonth.DayOfWeek + 7) % 7;
return firstOfMonth.AddDays(daysUntilThursday);
}
private DateTime GetNextWipeDateTime()
{
DateTime now = DateTime.Now;
DateTime firstThursday = GetFirstThursdayOfMonth(now.Year, now.Month);
DateTime wipeDateTime = new DateTime(firstThursday.Year, firstThursday.Month, firstThursday.Day, 13, 0, 0);
if (now > wipeDateTime)
{
DateTime nextMonth = now.AddMonths(1);
DateTime nextFirstThursday = GetFirstThursdayOfMonth(nextMonth.Year, nextMonth.Month);
wipeDateTime = new DateTime(nextFirstThursday.Year, nextFirstThursday.Month, nextFirstThursday.Day, 13, 0, 0);
}
return wipeDateTime;
}
private string FormatTimeRemaining(TimeSpan timeSpan)
{
if (timeSpan.TotalDays >= 1)
return $"{(int)timeSpan.TotalDays}d {timeSpan.Hours}h {timeSpan.Minutes}m";
else if (timeSpan.TotalHours >= 1)
return $"{timeSpan.Hours}h {timeSpan.Minutes}m";
else
return $"{timeSpan.Minutes}m";
}
private string GetFormattedNextWipeDate()
{
try
{
DateTime nextWipe = GetNextWipeDateTime();
TimeSpan timeUntil = nextWipe - DateTime.Now;
string timezone = GetServerTimeZoneAbbreviation();
if (nextWipe.Date == DateTime.Today)
{
if (timeUntil.TotalMinutes <= 0)
return $"Today 1:00 PM {timezone} (Wipe in progress!)";
else
return $"Today 1:00 PM {timezone} (in {FormatTimeRemaining(timeUntil)})";
}
else if (nextWipe.Date == DateTime.Today.AddDays(1))
{
return $"Tomorrow 1:00 PM {timezone} (in {FormatTimeRemaining(timeUntil)})";
}
else
{
return $"{nextWipe:MMM dd, yyyy} 1:00 PM {timezone} (in {FormatTimeRemaining(timeUntil)})";
}
}
catch (Exception ex)
{
PrintError($"Error formatting next wipe date: {ex.Message}");
return "Unknown";
}
}
#endregion
#region Population Update Timer Management
private void ResetPerformanceTimer()
{
try
{
performanceTimer?.Destroy();
if (config.Settings.EnablePerformanceMessages)
{
performanceTimer = timer.Every(config.Settings.PerformanceMessageInterval * 60f, SendPerformanceMessage);
Puts($"Performance timer reset - next update in {config.Settings.PerformanceMessageInterval} minutes");
}
}
catch (Exception ex)
{
PrintError($"Error resetting performance timer: {ex.Message}");
}
}
private void TriggerInstantPopulationUpdate()
{
try
{
if (!config.Settings.EnableInstantPopulationUpdates ||
!config.Settings.EnablePerformanceMessages ||
string.IsNullOrEmpty(config.Settings.WebhookURL))
{
return;
}
timer.Once(config.Settings.PopulationUpdateDelay, () =>
{
SendPerformanceMessage();
ResetPerformanceTimer();
});
Puts($"Instant population update triggered - Discord will update in {config.Settings.PopulationUpdateDelay} seconds");
}
catch (Exception ex)
{
PrintError($"Error triggering instant population update: {ex.Message}");
}
}
#endregion
#region Hooks
private void OnServerInitialized()
{
try
{
_isOnline = true;
Puts("Server marked as ONLINE");
LoadData();
try
{
string saveInfoPath = Path.Combine(World.SaveFolderName, $"player.blueprints.{Rust.Protocol.persistance}.db");
_saveInfo = SaveInfo.Create(saveInfoPath);
}
catch (Exception ex)
{
PrintError($"Error initializing blueprint save info: {ex.Message}");
}
if (config.Settings.EnablePerformanceMessages)
{
performanceTimer = timer.Every(config.Settings.PerformanceMessageInterval * 60f, SendPerformanceMessage);
timer.Once(10f, SendPerformanceMessage);
}
if (config.Settings.EnableInGamePerformanceMessages)
{
inGameMessageTimer = timer.Every(config.Settings.InGameMessageInterval * 60f, SendInGamePerformanceMessageOnly);
}
UpdatePopulationRecords();
}
catch (Exception ex)
{
PrintError($"Error during initialization: {ex.Message}");
}
}
private void OnServerShutdown()
{
try
{
_isOnline = false;
Puts("Server marked as OFFLINE - sending final Discord update");
if (config.Settings.EnablePerformanceMessages && !string.IsNullOrEmpty(config.Settings.WebhookURL))
{
SendPerformanceMessage();
}
}
catch (Exception ex)
{
PrintError($"Error during shutdown: {ex.Message}");
}
}
private void Unload()
{
performanceTimer?.Destroy();
inGameMessageTimer?.Destroy();
SaveData();
}
private void OnPlayerConnected(BasePlayer player)
{
timer.Once(1f, () =>
{
UpdatePopulationRecords();
TriggerInstantPopulationUpdate();
});
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
timer.Once(2f, () =>
{
UpdatePopulationRecords();
TriggerInstantPopulationUpdate();
});
}
private void OnPlayerChat(BasePlayer player, string message, ConVar.Chat.ChatChannel channel)
{
if (player == null || string.IsNullOrEmpty(message)) return;
if (message.ToLower() == "!pop")
HandlePopCommand(player);
}
#endregion
#region Pop Command
private void HandlePopCommand(BasePlayer player)
{
if (!config.Settings.EnablePopCommand)
{
player.ChatMessage("The !pop command is currently disabled.");
return;
}
var now = DateTime.Now;
var timeSinceLastUse = now - lastPopCommandTime;
if (timeSinceLastUse.TotalMinutes < config.Settings.CommandCooldown)
{
var remainingTime = TimeSpan.FromMinutes(config.Settings.CommandCooldown) - timeSinceLastUse;
player.ChatMessage($"Server statistics are on cooldown.Try again in {GetCooldownTime(remainingTime)}.");
return;
}
lastPopCommandTime = now;
int playerCount = BasePlayer.activePlayerList.Count;
int sleepingPlayers = BasePlayer.sleepingPlayerList.Count;
int joiningPlayers = ServerMgr.Instance.connectionQueue.Queued;
int maxPlayers = ConVar.Server.maxplayers;
int adminCount = BasePlayer.activePlayerList.Count(p => p.IsAdmin);
string statsMessage = $"<color=#FFD700><size=14>Population Stats:</size></color>\n" +
$"<color=#00FF00>Players Online:</color> <color=#FFFFFF>{playerCount}/{maxPlayers}</color>";
if (config.Settings.ShowPlayersJoining && (!config.Settings.HideZeroValues || joiningPlayers > 0))
statsMessage += $"\n<color=#FFFF00>Players Joining:</color> <color=#FFFFFF>{joiningPlayers}</color>";
if (config.Settings.ShowPlayersSleeping && (!config.Settings.HideZeroValues || sleepingPlayers > 0))
statsMessage += $"\n<color=#FF0000>Players Sleeping:</color> <color=#FFFFFF>{sleepingPlayers}</color>";
if (config.Settings.ShowAdminsOnline && (!config.Settings.HideZeroValues || adminCount > 0))
statsMessage += $"\n<color=#FFB6C1>Admins Online:</color> <color=#FFFFFF>{adminCount}</color>";
foreach (var onlinePlayer in BasePlayer.activePlayerList)
onlinePlayer?.ChatMessage(statsMessage);
}
#endregion
#region Discord Performance
private void SendPerformanceMessage()
{
try
{
int playerCount = BasePlayer.activePlayerList.Count;
int sleepingPlayers = BasePlayer.sleepingPlayerList.Count;
int joiningPlayers = ServerMgr.Instance.connectionQueue.Queued;
int maxPlayers = ConVar.Server.maxplayers;
int adminCount = BasePlayer.activePlayerList.Count(p => p.IsAdmin);
float fps = Performance.current.frameRate;
long memoryUsed = GC.GetTotalMemory(false) / 1024 / 1024;
long totalMemory = SystemInfo.systemMemorySize;
int mapEntities = BaseNetworkable.serverEntities.Count;
string uptime = GetHumanReadableUptime();
string lastWipeDate = GetLastWipeDate();
int totalPlayersEver = GetTotalPlayersEver();
string averageConnectionTime = GetAverageConnectionTime();
string networkIO = GetNetworkIO();
string lastBpWipeDate = GetFormattedBlueprintWipeDate();
string nextWipeDate = GetFormattedNextWipeDate();
string message = "";
if (config.Settings.ShowServerStatus)
{
message += $"{GetServerStatusEmoji()} **Server Status:** `{GetServerStatusText()}`\n\n";
}
message += $"`📊 Population Data`\n";
message += $"🟢 **Players Online:** `{playerCount}/{maxPlayers}`";
if (config.Settings.ShowPlayersJoining && (!config.Settings.HideZeroValues || joiningPlayers > 0))
message += $"\n🟡 **Players In Queue:** `{joiningPlayers}`";
if (config.Settings.ShowPlayersSleeping && (!config.Settings.HideZeroValues || sleepingPlayers > 0))
message += $"\n🔴 **Players Sleeping:** `{sleepingPlayers}`";
if (config.Settings.ShowAdminsOnline && (!config.Settings.HideZeroValues || adminCount > 0))
message += $"\n👑 **Admins Online:** `{adminCount}`";
if (config.Settings.ShowPopulationRecords)
{
if (pluginData.TodayHigh.Count > 0)
message += $"\n📈 **Today's Peak Players:** `{pluginData.TodayHigh.Count}`";
if (pluginData.MonthlyHigh.Count > 0)
message += $"\n📊 **Monthly Peak Players:** `{pluginData.MonthlyHigh.Count}`";
if (pluginData.AllTimeHigh.Count > 0)
message += $"\n🏆 **All-Time Peak Players:** `{pluginData.AllTimeHigh.Count}`";
}
if (config.Settings.ShowTotalPlayersEver)
message += $"\n🏢 **Total Server Players:** `{totalPlayersEver:N0}`";
if (config.Settings.ShowAverageConnectionTime && playerCount > 0)
message += $"\n⏱️ **Average Active Session Time:** `{averageConnectionTime}`";
message += $"\n\n`🌍 World Data`\n" +
$"🕒 **In-Game Time:** `{GetInGameTime()}`\n" +
$"🌍 **World Size:** `{ConVar.Server.worldsize}`\n" +
$"🌱 **Seed:** `{ConVar.Server.seed}`\n" +
$"🏗️ **Map Entities:** `{mapEntities:N0}`";
if (config.Settings.ShowProtocol)
message += $"\n🔗 **Protocol:** `{GetServerProtocol()}`";
message += $"\n\n`🖥️ Server Data`\n" +
$"💾 **Memory Usage:** `{memoryUsed:N0} MB / {totalMemory:N0} MB`\n" +
$"⚡ **Server FPS:** `{fps:F1}`";
if (config.Settings.ShowNetworkIO)
message += $"\n🌐 **Network IO:** `{networkIO}`";
message += $"\n🕐 **Server Online For:** `{uptime}`\n" +
$"🗺️ **Last Wipe:** `{lastWipeDate}`";
if (config.Settings.ShowNextWipeDate)
message += $"\n📅 **Next Wipe:** `{nextWipeDate}`";
if (config.Settings.ShowLastBlueprintWipeDate)
message += $"\n📘 **Last BP Wipe:** `{lastBpWipeDate}`";
int embedColor = _isOnline ? 65535 : 16711680;
string title = "Live Server Statistics";
SendOrEditDiscordMessage(title, message, embedColor);
}
catch (Exception ex)
{
PrintError($"Error sending performance message: {ex.Message}");
}
}
private void SendInGamePerformanceMessageOnly()
{
try
{
int playerCount = BasePlayer.activePlayerList.Count;
int sleepingPlayers = BasePlayer.sleepingPlayerList.Count;
int joiningPlayers = ServerMgr.Instance.connectionQueue.Queued;
int maxPlayers = ConVar.Server.maxplayers;
int adminCount = BasePlayer.activePlayerList.Count(p => p.IsAdmin);
int totalPlayersEver = GetTotalPlayersEver();
SendInGamePerformanceMessage(playerCount, sleepingPlayers, joiningPlayers, maxPlayers, adminCount, totalPlayersEver);
}
catch (Exception ex)
{
PrintError($"Error sending in-game performance message: {ex.Message}");
}
}
private void SendInGamePerformanceMessage(int playerCount, int sleepingPlayers, int joiningPlayers, int maxPlayers, int adminCount, int totalPlayersEver)
{
try
{
string performanceMessage = $"<color=#FFD700><size=14>Population Stats:</size></color>\n" +
$"<color=#00FF00>Players Online:</color> <color=#FFFFFF>{playerCount}/{maxPlayers}</color>";
if (config.Settings.ShowPlayersJoining && (!config.Settings.HideZeroValues || joiningPlayers > 0))
performanceMessage += $"\n<color=#FFFF00>Players In Queue:</color> <color=#FFFFFF>{joiningPlayers}</color>";
if (config.Settings.ShowPlayersSleeping && (!config.Settings.HideZeroValues || sleepingPlayers > 0))
performanceMessage += $"\n<color=#FF0000>Players Sleeping:</color> <color=#FFFFFF>{sleepingPlayers}</color>";
if (config.Settings.ShowAdminsOnline && (!config.Settings.HideZeroValues || adminCount > 0))
performanceMessage += $"\n<color=#FFB6C1>Admins Online:</color> <color=#FFFFFF>{adminCount}</color>";
if (config.Settings.ShowTotalPlayersEver)
performanceMessage += $"\n<color=#87CEEB>Total Server Players:</color> <color=#FFFFFF>{totalPlayersEver:N0}</color>";
foreach (var player in BasePlayer.activePlayerList)
player?.ChatMessage(performanceMessage);
}
catch (Exception ex)
{
PrintError($"Error sending in-game performance message: {ex.Message}");
}
}
#endregion
#region Discord Integration
private bool IsDiscordRateLimited(string messageType)
{
if (lastDiscordMessage.TryGetValue(messageType, out DateTime lastTime))
{
if ((DateTime.Now - lastTime).TotalSeconds < config.Settings.DiscordRateLimit)
return true;
}
lastDiscordMessage[messageType] = DateTime.Now;
return false;
}
private string GetServerImageUrl()
{
try
{
if (config.Settings.UseServerHeaderImage)
{
string headerImage = ConVar.Server.headerimage;
if (!string.IsNullOrEmpty(headerImage) && IsValidImageUrl(headerImage))
return headerImage;
}
return config.Settings.FallbackImageURL;
}
catch
{
return config.Settings.FallbackImageURL;
}
}
private bool IsValidImageUrl(string url)
{
if (string.IsNullOrEmpty(url)) return false;
try
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri result)) return false;
if (result.Scheme != "http" && result.Scheme != "https") return false;
string[] imageExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp" };
string path = result.AbsolutePath.ToLower();
return imageExtensions.Any(ext => path.EndsWith(ext)) ||
path.Contains("image") ||
result.Host.Contains("imgur") ||
result.Host.Contains("steam") ||
result.Host.Contains("facepunch");
}
catch
{
return false;
}
}
private void SendOrEditDiscordMessage(string title, string description, int color)
{
try
{
string webhookUrl = config.Settings.WebhookURL;
if (string.IsNullOrEmpty(webhookUrl)) return;
if (IsDiscordRateLimited("performance")) return;
string serverName = ConVar.Server.hostname ?? "Unknown Server";
string serverImageUrl = GetServerImageUrl();
string displayServerName = serverName.Length > 45
? serverName.Substring(0, 42) + "..."
: serverName;
string embedTitle = $"[{displayServerName}]\n\n🤖 Live Server Statistics";
var embed = new
{
title = embedTitle,
description = description,
color = color,
thumbnail = config.Settings.ShowDiscordImage && config.Settings.UseThumbnail ? new { url = serverImageUrl } : null,
image = config.Settings.ShowDiscordImage && !config.Settings.UseThumbnail ? new { url = serverImageUrl } : null,
footer = new { text = $"rPop Live Server Statistics V{Version} by Ftuoil Xelrash" },
timestamp = DateTime.UtcNow.ToString("o")
};
var payload = JsonConvert.SerializeObject(new
{
username = config.Settings.DiscordBotName,
avatar_url = "https://cdn-icons-png.flaticon.com/512/1161/1161388.png",
embeds = new[] { embed }
});
var headers = new Dictionary<string, string>
{
["Content-Type"] = "application/json",
["User-Agent"] = "rPop/1.0"
};
if (!string.IsNullOrEmpty(pluginData.StatusMessageId))
{
string editUrl = $"{webhookUrl}/messages/{pluginData.StatusMessageId}";
webrequest.Enqueue(editUrl, payload, (code, response) =>
{
if (code == 200 || code == 204)
{
// Message successfully edited
}
else if (code == 404)
{
Puts("Discord status message not found, creating new one...");
pluginData.StatusMessageId = null;
SaveData();
CreateNewDiscordMessage(webhookUrl, payload, headers);
}
else
{
PrintError($"Failed to edit Discord message: HTTP {code} - {response}");
}
}, this, Core.Libraries.RequestMethod.PATCH, headers);
}
else
{
CreateNewDiscordMessage(webhookUrl, payload, headers);
}
}
catch (Exception ex)
{
PrintError($"Error sending/editing Discord message: {ex.Message}");
}
}
private void CreateNewDiscordMessage(string webhookUrl, string payload, Dictionary<string, string> headers)
{
webrequest.Enqueue(webhookUrl + "?wait=true", payload, (code, response) =>
{
if (code == 200)
{
try
{
var messageData = JsonConvert.DeserializeObject<Dictionary<string, object>>(response);
if (messageData.ContainsKey("id"))
{
pluginData.StatusMessageId = messageData["id"].ToString();
SaveData();
Puts($"Created new Discord status message with ID: {pluginData.StatusMessageId}");
}
}
catch (Exception ex)
{
PrintError($"Error parsing Discord message response: {ex.Message}");
}
}
else
{
PrintError($"Failed to create Discord message: HTTP {code} - {response}");
}
}, this, Core.Libraries.RequestMethod.POST, headers);
}
#endregion
#region Utilities
private string GetHumanReadableUptime()
{
TimeSpan uptime = TimeSpan.FromSeconds(UnityEngine.Time.realtimeSinceStartup);
if (uptime.TotalDays >= 1)
return $"{(int)uptime.TotalDays} days, {uptime.Hours} hours, {uptime.Minutes} minutes";
else if (uptime.TotalHours >= 1)
return $"{uptime.Hours} hours, {uptime.Minutes} minutes";
else if (uptime.TotalMinutes >= 1)
return $"{uptime.Minutes} minutes, {uptime.Seconds} seconds";
else
return $"{uptime.Seconds} seconds";
}
private string GetLastWipeDate()
{
try
{
DateTime wipeTime = GetWipeTimeFromSaveFile();
if (wipeTime == DateTime.MinValue)
{
TimeSpan uptime = TimeSpan.FromSeconds(UnityEngine.Time.realtimeSinceStartup);
DateTime serverStart = DateTime.Now - uptime;
retu
// ... [Code preview truncated due to length. Full source in attached file] ...