C#:
// Все права принадлежат дискорд сообществу https://discord.gg/VgNHPpNrz6
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Admin Monitor", "Braga", "1.3.0")]
[Description("Monitors admin activity and tracks their time spent in-game")]
public class AdminMonitor : RustPlugin
{
#region Fields
private const string PermissionAdmin = "adminmonitor.admin";
private const string PermissionView = "adminmonitor.view";
private const string PermissionTracked = "adminmonitor.tracked";
private const string PermissionReports = "adminmonitor.reports";
private StoredData? storedData;
private readonly Dictionary<ulong, AdminActivity> activeAdmins = new();
private readonly Dictionary<ulong, Timer> activityTimers = new();
private readonly Dictionary<ulong, Timer> uiUpdateTimers = new();
private readonly Dictionary<ulong, Vector3> lastPositions = new();
private readonly Dictionary<ulong, Quaternion> lastRotations = new();
private readonly Dictionary<ulong, float> lastActionTimes = new();
private readonly Dictionary<ulong, HashSet<string>> activeActions = new();
private readonly string LayerMain = "AdminMonitor.UI";
private readonly string LayerContent = "AdminMonitor.Content";
[PluginReference]
private readonly Plugin? ImageLibrary;
private readonly Dictionary<ulong, string> avatarUrls = new();
/// <summary>
/// URL Discord вебхука для отправки уведомлений
/// </summary>
private string DiscordWebhookUrl => Config["DiscordWebhookUrl"]?.ToString() ?? string.Empty;
/// <summary>
/// Порог движения для определения AFK (в единицах)
/// </summary>
private const float MovementThreshold = 0.1f;
/// <summary>
/// Порог поворота для определения AFK (в градусах)
/// </summary>
private const float RotationThreshold = 1f;
/// <summary>
/// Интервал проверки активности в секундах
/// </summary>
private readonly float checkInterval = 1f;
/// <summary>
/// Порог неактивности в секундах
/// </summary>
private readonly float inactivityThreshold = 60f;
private readonly Dictionary<ulong, List<AdminAction>> recentActionsCache = new();
private readonly TimeSpan actionsCacheExpiration = TimeSpan.FromSeconds(5);
private readonly Dictionary<ulong, DateTime> lastActionsCacheUpdate = new();
private readonly Dictionary<string, string> filterSettings = new();
private const string FilterAll = "all";
private const string FilterOnline = "online";
private const string FilterOffline = "offline";
private const string FilterAfk = "afk";
private const string SortTimeActive = "active";
private const string SortTimeAfk = "afk";
private const string SortName = "name";
private const string ReportsFolderName = "Reports";
#endregion Fields
#region Data Classes
private sealed class StoredData
{
public Dictionary<ulong, AdminStats> AdminStatistics { get; set; } = new();
public List<WipeHistory> WipeHistory { get; set; } = new();
public DateTime? CurrentWipeStart { get; set; }
}
private sealed class WipeHistory
{
public required DateTime StartDate { get; set; }
public required DateTime EndDate { get; set; }
public Dictionary<ulong, AdminWipeStats> AdminStats { get; set; } = new();
}
private sealed class AdminWipeStats
{
public required string LastName { get; set; }
public float TotalActiveTime { get; set; }
public float TotalAfkTime { get; set; }
public Dictionary<string, int> CommandsUsed { get; set; } = new();
public DateTime LastActive { get; set; }
}
private sealed class AdminStats
{
public required string LastName { get; set; }
public float TotalActiveTime { get; set; }
public float TotalAfkTime { get; set; }
public Dictionary<string, int> CommandsUsed { get; set; } = new();
public List<AdminAction> RecentActions { get; set; } = new();
public DateTime LastActive { get; set; }
public float CurrentWipeActiveTime { get; set; }
public float CurrentWipeAfkTime { get; set; }
}
private sealed class AdminActivity
{
public float SessionStartTime { get; set; }
public float LastActiveTime { get; set; }
public float TotalAfkTime { get; set; }
public bool IsAfk { get; set; }
public Vector3 LastPosition { get; set; }
public float AfkStartTime { get; set; }
}
private sealed class AdminAction
{
public required string ActionType { get; set; }
public required string Details { get; set; }
public required string Target { get; set; }
public DateTime Timestamp { get; set; }
public Vector3 Location { get; set; }
}
private sealed class AdminReport
{
public required DateTime GenerationDate { get; set; }
public required string ReportType { get; set; }
public required string Period { get; set; }
public Dictionary<ulong, AdminReportStats> Statistics { get; set; } = new();
}
private sealed class AdminReportStats
{
public required string AdminName { get; set; }
public float ActiveTime { get; set; }
public float AfkTime { get; set; }
public int CommandsUsed { get; set; }
public int BuildActions { get; set; }
public int DestroyActions { get; set; }
public int PlayerInteractions { get; set; }
public Dictionary<string, int> ActionTypes { get; set; } = new();
}
#endregion Data Classes
#region Oxide Hooks
private void Init()
{
permission.RegisterPermission(PermissionAdmin, this);
permission.RegisterPermission(PermissionView, this);
permission.RegisterPermission(PermissionTracked, this);
permission.RegisterPermission(PermissionReports, this);
LoadData();
cmd.AddChatCommand("amonitor", this, nameof(CmdAdminMonitor));
cmd.AddConsoleCommand("amonitor.check", this, nameof(CmdCheckAdmin));
cmd.AddConsoleCommand("amonitor.report", this, nameof(CmdGenerateReport));
// Создаем папку для отчетов
if (
!Interface.Oxide.DataFileSystem.ExistsDatafile(
$"{DataFolderName}/{ReportsFolderName}"
)
)
{
_ = Interface.Oxide.DataFileSystem.GetDatafile(
$"{DataFolderName}/{ReportsFolderName}"
);
}
// Добавляем таймер для сохранения статистики каждую минуту
_ = timer.Every(
60f,
() =>
{
if (storedData == null)
{
return;
}
UpdateAllAdminStats();
SaveData();
Puts("Admin statistics saved (1-minute interval)");
}
);
ScheduleReports();
}
private void OnServerInitialized(bool initial)
{
if (ImageLibrary == null)
{
PrintError("ImageLibrary plugin is not installed!");
}
if (storedData != null && storedData.CurrentWipeStart == null)
{
storedData.CurrentWipeStart = DateTime.Now;
SaveData();
}
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
if (player?.IsAdmin == true)
{
StartMonitoring(player);
LoadPlayerAvatar(player);
}
}
}
private void OnNewSave(string filename)
{
if (storedData == null)
{
return;
}
// Сохраняем статистику предыдущего вайпа
if (storedData.CurrentWipeStart.HasValue)
{
WipeHistory wipeHistory = new()
{
StartDate = storedData.CurrentWipeStart.Value,
EndDate = DateTime.Now,
};
foreach (KeyValuePair<ulong, AdminStats> kvp in storedData.AdminStatistics)
{
wipeHistory.AdminStats[kvp.Key] = new AdminWipeStats
{
LastName = kvp.Value.LastName,
TotalActiveTime = kvp.Value.CurrentWipeActiveTime,
TotalAfkTime = kvp.Value.CurrentWipeAfkTime,
CommandsUsed = new Dictionary<string, int>(kvp.Value.CommandsUsed),
LastActive = kvp.Value.LastActive,
};
// Сбрасываем статистику текущего вайпа
kvp.Value.CurrentWipeActiveTime = 0;
kvp.Value.CurrentWipeAfkTime = 0;
kvp.Value.CommandsUsed.Clear();
}
storedData.WipeHistory.Add(wipeHistory);
storedData.CurrentWipeStart = DateTime.Now;
SaveData();
}
}
private void OnServerSave()
{
if (storedData == null)
{
return;
}
UpdateAllAdminStats();
SaveData();
Puts("Admin statistics saved (server save)");
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null)
{
return;
}
if (
player.IsAdmin
|| permission.UserHasPermission(player.UserIDString, PermissionTracked)
)
{
StartMonitoring(player);
LoadPlayerAvatar(player);
SendDiscordLoginMessage(player);
}
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (
player == null
|| (
!player.IsAdmin
&& !permission.UserHasPermission(player.UserIDString, PermissionTracked)
)
)
{
return;
}
// Очищаем таймер обновления UI при отключении
if (uiUpdateTimers.TryGetValue(player.userID, out Timer uiTimer))
{
uiTimer.Destroy();
_ = uiUpdateTimers.Remove(player.userID);
}
SendDiscordLogoutMessage(player);
StopMonitoring(player);
SaveAdminStats(player);
}
private object? OnPlayerCommand(BasePlayer? player, string command, string[] args)
{
if (
player == null
|| (
!player.IsAdmin
&& !permission.UserHasPermission(player.UserIDString, PermissionTracked)
)
)
{
return null;
}
RecordAction(player, "Command", command, string.Join(" ", args));
UpdateAdminStats(player, "command", command);
return null;
}
private void OnPlayerViolation(BasePlayer? player, AntiHackType type, float amount, BaseEntity? entity)
{
if (player?.IsAdmin != true)
{
return;
}
RecordAction(player, "Violation", $"{type}", $"Amount: {amount}");
}
private void OnEntityBuilt(Planner planner, GameObject go)
{
if (planner == null || go == null)
{
return;
}
BasePlayer? player = planner.GetOwnerPlayer();
if (player?.IsAdmin != true)
{
return;
}
string objectName = go.name ?? "unknown";
RecordAction(player, "Build", objectName, "");
}
private void OnEntityKill(BaseCombatEntity entity)
{
if (entity == null)
{
return;
}
BasePlayer? lastAttacker = entity.lastAttacker as BasePlayer;
if (lastAttacker?.IsAdmin != true)
{
return;
}
RecordAction(lastAttacker, "Destroy", entity.ShortPrefabName, "");
}
private void OnPlayerAttack(BasePlayer? attacker, HitInfo? info)
{
if (attacker?.IsAdmin != true)
{
return;
}
BasePlayer? victim = info?.HitEntity as BasePlayer;
if (victim != null)
{
RecordAction(attacker, "Attack", "Player", victim.displayName);
}
}
private void OnPlayerInput(BasePlayer player, InputState input)
{
if (
player == null
|| input == null
|| (
!player.IsAdmin
&& !permission.UserHasPermission(player.UserIDString, PermissionTracked)
)
)
{
return;
}
if (!activeActions.TryGetValue(player.userID, out HashSet<string> actions))
{
actions = new HashSet<string>();
activeActions[player.userID] = actions;
}
// Отслеживаем нажатия клавиш
if (input.WasJustPressed(BUTTON.FIRE_PRIMARY))
{
_ = actions.Add("combat");
}
if (input.WasJustPressed(BUTTON.FIRE_SECONDARY))
{
_ = actions.Add("combat");
}
if (input.WasJustPressed(BUTTON.USE))
{
_ = actions.Add("interaction");
}
if (input.WasJustPressed(BUTTON.SPRINT))
{
_ = actions.Add("movement");
}
// Очищаем действия через небольшую задержку
_ = timer.Once(
1f,
() =>
{
if (actions.Count > 0)
{
actions.Clear();
}
}
);
}
private void OnUserPermissionGranted(string id, string permName)
{
if (permName != PermissionTracked)
{
return;
}
BasePlayer? player = BasePlayer.Find(id);
if (player == null)
{
return;
}
// Начинаем отслеживание только если игрок не является админом
if (!player.IsAdmin)
{
StartMonitoring(player);
LoadPlayerAvatar(player);
SendReply(player, "Вы добавлены в систему мониторинга активности.");
}
}
private void OnUserPermissionRevoked(string id, string permName)
{
if (permName != PermissionTracked)
{
return;
}
BasePlayer? player = BasePlayer.Find(id);
if (player == null)
{
return;
}
// Останавливаем отслеживание только если игрок не является админом
if (!player.IsAdmin)
{
StopMonitoring(player);
SendReply(player, "Вы удалены из системы мониторинга активности.");
}
}
#endregion Oxide Hooks
#region Core Functions
private void StartMonitoring(BasePlayer player)
{
if (
player == null
|| (
!player.IsAdmin
&& !permission.UserHasPermission(player.UserIDString, PermissionTracked)
)
)
{
return;
}
// Если уже отслеживается - не начинаем заново
if (activeAdmins.ContainsKey(player.userID))
{
return;
}
activeAdmins[player.userID] = new()
{
SessionStartTime = Time.realtimeSinceStartup,
LastActiveTime = Time.realtimeSinceStartup,
LastPosition = player.transform.position,
IsAfk = false,
};
lastPositions[player.userID] = player.transform.position;
lastActionTimes[player.userID] = Time.realtimeSinceStartup;
activityTimers[player.userID] = timer.Every(checkInterval, () => CheckActivity(player));
if (
storedData?.AdminStatistics != null
&& !storedData.AdminStatistics.ContainsKey(player.userID)
)
{
storedData.AdminStatistics[player.userID] = new AdminStats
{
LastName = player.displayName,
LastActive = DateTime.Now,
};
}
RecordAction(player, "Login", "Session Start", "");
}
private void StopMonitoring(BasePlayer player)
{
if (player == null)
{
return;
}
if (activityTimers.TryGetValue(player.userID, out Timer timer))
{
timer.Destroy();
_ = activityTimers.Remove(player.userID);
}
if (activeAdmins.ContainsKey(player.userID))
{
AdminActivity activity = activeAdmins[player.userID];
float sessionTime =
Time.realtimeSinceStartup - activity.SessionStartTime - activity.TotalAfkTime;
if (
storedData?.AdminStatistics != null
&& storedData.AdminStatistics.TryGetValue(player.userID, out AdminStats stats)
)
{
stats.TotalActiveTime += sessionTime;
stats.TotalAfkTime += activity.TotalAfkTime;
stats.LastActive = DateTime.Now;
}
_ = activeAdmins.Remove(player.userID);
}
_ = lastPositions.Remove(player.userID);
_ = lastActionTimes.Remove(player.userID);
RecordAction(player, "Logout", "Session End", "");
SaveData();
}
private void CheckActivity(BasePlayer player)
{
if (player?.IsConnected != true || !activeAdmins.ContainsKey(player.userID))
{
return;
}
AdminActivity activity = activeAdmins[player.userID];
Vector3 currentPos = player.transform.position;
Quaternion currentRot = player.eyes.rotation;
float currentTime = Time.realtimeSinceStartup;
// Проверяем различные типы активности
bool hasMoved = Vector3.Distance(activity.LastPosition, currentPos) > MovementThreshold;
bool hasRotated =
Quaternion.Angle(
lastRotations.TryGetValue(player.userID, out Quaternion lastRot)
? lastRot
: currentRot,
currentRot
) > RotationThreshold;
bool hasActiveActions =
activeActions.TryGetValue(player.userID, out HashSet<string> actions)
&& actions.Count > 0;
bool isInInventory = player.inventory.loot.IsLooting();
bool isInCrafting = player.inventory.crafting.queue.Count > 0;
bool isActive =
hasMoved || hasRotated || hasActiveActions || isInInventory || isInCrafting;
if (!isActive)
{
if (
!activity.IsAfk
&& (currentTime - activity.LastActiveTime) >= inactivityThreshold
)
{
// Игрок только что стал AFK
activity.IsAfk = true;
activity.AfkStartTime = currentTime;
RecordAction(
player,
"Status",
"AFK Start",
GetAfkReason(
hasMoved,
hasRotated,
hasActiveActions,
isInInventory,
isInCrafting
)
);
SendDiscordStatusChangeMessage(player, true);
// Отправляем уведомление игроку
SendReply(player, "Вы перешли в режим AFK");
}
}
else if (activity.IsAfk)
{
// Игрок вернулся из AFK
float afkDuration = currentTime - activity.AfkStartTime;
activity.TotalAfkTime += afkDuration;
activity.IsAfk = false;
activity.LastActiveTime = currentTime;
RecordAction(player, "Status", "AFK End", $"Duration: {afkDuration:F1}s");
SendDiscordStatusChangeMessage(player, false, afkDuration);
SendReply(
player,
$"Вы вернулись из AFK режима.Время отсутствия: {FormatTime(afkDuration)}"
);
}
// Обновляем последние известные позиции и время активности
if (isActive)
{
activity.LastActiveTime = currentTime;
}
activity.LastPosition = currentPos;
lastRotations[player.userID] = currentRot;
}
private string GetAfkReason(
bool hasMoved,
bool hasRotated,
bool hasActiveActions,
bool isInInventory,
bool isInCrafting
)
{
List<string> reasons = new();
if (!hasMoved && !hasRotated)
{
reasons.Add("отсутствие движения");
}
if (!hasActiveActions)
{
reasons.Add("нет активных действий");
}
if (!isInInventory && !isInCrafting)
{
reasons.Add("нет взаимодействия с инвентарем");
}
return string.Join(", ", reasons);
}
private void UpdateAdminStats(BasePlayer? player, string type, string detail)
{
if (player == null || storedData == null || player.displayName == null)
{
return;
}
string displayName = player.displayName;
ulong userId = player.userID;
if (!storedData.AdminStatistics.TryGetValue(userId, out AdminStats stats))
{
stats = new AdminStats
{
LastName = displayName,
CommandsUsed = new Dictionary<string, int>(),
RecentActions = new List<AdminAction>(),
};
storedData.AdminStatistics[userId] = stats;
}
if (type == "command")
{
if (!stats.CommandsUsed.TryGetValue(detail, out int count))
{
count = 0;
}
stats.CommandsUsed[detail] = count + 1;
}
stats.LastActive = DateTime.Now;
SaveAdminData(userId);
}
private void RecordAction(
BasePlayer? player,
string actionType,
string details,
string target
)
{
if (
player == null
|| storedData == null
|| player.displayName == null
|| player.transform == null
)
{
return;
}
string displayName = player.displayName;
ulong userId = player.userID;
Vector3 position = player.transform.position;
if (!storedData.AdminStatistics.TryGetValue(userId, out AdminStats stats))
{
stats = new AdminStats
{
LastName = displayName,
RecentActions = new List<AdminAction>(),
};
storedData.AdminStatistics[userId] = stats;
}
AdminAction action = new()
{
ActionType = actionType,
Details = details,
Target = target,
Timestamp = DateTime.Now,
Location = position,
};
stats.RecentActions.Add(action);
while (stats.RecentActions.Count > 100)
{
stats.RecentActions.RemoveAt(0);
}
SaveAdminData(userId);
}
private void SaveAdminStats(BasePlayer player)
{
if (storedData == null)
{
return;
}
if (!storedData.AdminStatistics.TryGetValue(player.userID, out AdminStats stats))
{
return;
}
if (activeAdmins.TryGetValue(player.userID, out AdminActivity activity))
{
float currentTime = Time.realtimeSinceStartup;
float sessionDuration = currentTime - activity.SessionStartTime;
float totalAfkTime = activity.TotalAfkTime;
// Если игрок был AFK в момент выхода, добавляем последнее время AFK
if (activity.IsAfk)
{
totalAfkTime += currentTime - activity.AfkStartTime;
}
float activeTime = sessionDuration - totalAfkTime;
if (activeTime > 0)
{
stats.TotalActiveTime += activeTime;
stats.CurrentWipeActiveTime += activeTime;
}
if (totalAfkTime > 0)
{
stats.TotalAfkTime += totalAfkTime;
stats.CurrentWipeAfkTime += totalAfkTime;
}
}
stats.LastActive = DateTime.Now;
SaveAdminData(player.userID);
}
private void UpdateAllAdminStats()
{
if (storedData == null)
{
return;
}
float currentTime = Time.realtimeSinceStartup;
// Обновляем статистику для всех активных администраторов
foreach (KeyValuePair<ulong, AdminActivity> kvp in activeAdmins)
{
if (storedData.AdminStatistics.TryGetValue(kvp.Key, out AdminStats stats))
{
float sessionDuration = currentTime - kvp.Value.SessionStartTime;
float afkTime = kvp.Value.TotalAfkTime;
float activeTime;
if (kvp.Value.IsAfk)
{
// Если админ сейчас AFK:
// 1.Считаем активное время до начала AFK
activeTime =
kvp.Value.AfkStartTime
- kvp.Value.SessionStartTime
- kvp.Value.TotalAfkTime;
// 2.Добавляем текущее время AFK
afkTime += currentTime - kvp.Value.AfkStartTime;
}
else
{
// Если админ активен:
// Считаем чистое активное время (общее время минус все время AFK)
activeTime = sessionDuration - afkTime;
}
// Обновляем статистику только если есть изменения
if (activeTime > 0)
{
stats.CurrentWipeActiveTime += activeTime;
stats.TotalActiveTime += activeTime;
}
if (afkTime > 0)
{
stats.CurrentWipeAfkTime += afkTime;
stats.TotalAfkTime += afkTime;
}
// Сбрасываем счетчики сессии
kvp.Value.SessionStartTime = currentTime;
kvp.Value.TotalAfkTime = 0;
if (kvp.Value.IsAfk)
{
kvp.Value.AfkStartTime = currentTime;
}
}
}
}
#endregion Core Functions
#region Commands
private void CmdAdminMonitor(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermissionView))
{
SendReply(player, "У вас нет прав для использования этой команды.");
return;
}
if (args.Length == 0)
{
ShowAdminList(player);
return;
}
switch (args[0].ToLower(CultureInfo.CurrentCulture))
{
case "check":
if (args.Length < 2)
{
SendReply(player, "Использование: /amonitor check <имя/id>");
return;
}
CheckAdminStats(player, args[1]);
break;
case "reset":
if (!permission.UserHasPermission(player.UserIDString, PermissionAdmin))
{
SendReply(player, "У вас нет прав для сброса статистики.");
return;
}
ResetStats();
SendReply(player, "Статистика администраторов сброшена.");
break;
default:
SendReply(
player,
"Доступные команды:\n/amonitor - список администраторов\n/amonitor check <имя/id> - проверить статистику\n/amonitor reset - сбросить статистику"
);
break;
}
}
[ConsoleCommand("amonitor.check")]
private void CmdCheckAdmin(ConsoleSystem.Arg arg)
{
if (
arg.Player() != null
&& !permission.UserHasPermission(arg.Player().UserIDString, PermissionView)
)
{
arg.ReplyWith("У вас нет прав для использования этой команды.");
return;
}
if (!arg.HasArgs())
{
arg.ReplyWith("Использование: amonitor.check <steamid>");
return;
}
if (!ulong.TryParse(arg.GetString(0), out ulong targetId))
{
arg.ReplyWith("Неверный формат SteamID.");
return;
}
if (storedData?.AdminStatistics == null)
{
arg.ReplyWith("Ошибка: база данных не инициализирована.");
return;
}
if (!storedData.AdminStatistics.TryGetValue(targetId, out AdminStats stats))
{
arg.ReplyWith("Статистика для указанного администратора не найдена.");
return;
}
StringBuilder reply = new();
_ = reply.AppendLine($"Статистика администратора: {stats.LastName}");
float totalActiveTime = stats.TotalActiveTime;
float totalAfkTime = stats.TotalAfkTime;
if (activeAdmins.TryGetValue(targetId, out AdminActivity activity))
{
float currentTime = Time.realtimeSinceStartup;
if (activity.IsAfk)
{
// Если админ сейчас AFK:
// 1.Добавляем активное время до начала AFK
float activeTimeBeforeAfk =
activity.AfkStartTime - activity.SessionStartTime - activity.TotalAfkTime;
if (activeTimeBeforeAfk > 0)
{
totalActiveTime += activeTimeBeforeAfk;
}
// 2.Добавляем текущее время AFK
float currentAfkTime = currentTime - activity.AfkStartTime;
if (currentAfkTime > 0)
{
totalAfkTime += activity.TotalAfkTime + currentAfkTime;
}
}
else
{
// Если админ активен:
// 1.Добавляем только активное время текущей сессии
float sessionDuration = currentTime - activity.SessionStartTime;
float activeTimeInSession = sessionDuration - activity.TotalAfkTime;
if (activeTimeInSession > 0)
{
totalActiveTime += activeTimeInSession;
}
// 2.Добавляем только накопленное время AFK (без текущего времени, т.к.админ активен)
totalAfkTime += activity.TotalAfkTime;
}
}
_ = reply.AppendLine($"Активное время: {FormatTime(totalActiveTime)}");
_ = reply.AppendLine($"Время AFK: {FormatTime(totalAfkTime)}");
_ = reply.AppendLine($"Последняя активность: {FormatDateTime(stats.LastActive)}");
if (stats.CommandsUsed.Count > 0)
{
_ = reply.AppendLine("\nИспользованные команды:");
foreach (
KeyValuePair<string, int> cmd in stats.CommandsUsed.OrderByDescending(x => x.Value).Take(10)
)
{
_ = reply.AppendLine($"- {cmd.Key}: {cmd.Value} раз");
}
}
if (stats.RecentActions.Count > 0)
{
_ = reply.AppendLine("\nПоследние действия:");
foreach (AdminAction action in GetCachedRecentActions(targetId, stats, 5))
{
_ = reply.AppendLine(
$"- [{action.Timestamp:HH:mm:ss}] {action.ActionType}: {action.Details} {(string.IsNullOrEmpty(action.Target) ? "" : $"-> {action.Target}")}"
);
}
}
arg.ReplyWith(reply.ToString());
}
#endregion Commands
#region Helper Functions
private void ShowAdminList(BasePlayer player)
{
if (storedData == null)
{
return;
}
// Очищаем предыдущий таймер обновления UI если есть
if (uiUpdateTimers.TryGetValue(player.userID, out Timer oldTimer))
{
oldTimer.Destroy();
_ = uiUpdateTimers.Remove(player.userID);
}
// Создаем новый таймер для обновления UI каждую секунду
uiUpdateTimers[player.userID] = timer.Every(
1f,
() =>
{
if (!player.IsConnected)
{
if (uiUpdateTimers.TryGetValue(player.userID, out Timer timer))
{
timer.Destroy();
_ = uiUpdateTimers.Remove(player.userID);
}
return;
}
// Обновляем статистику перед обновлением UI
UpdateAllAdminStats();
// Обновляем UI
UpdateAdminListUI(player);
}
);
// Показываем UI первый раз
UpdateAllAdminStats();
UpdateAdminListUI(player);
}
private void UpdateAdminListUI(BasePlayer player)
{
if (storedData == null || !player.IsConnected)
{
return;
}
UpdateAllAdminStats();
_ = CuiHelper.DestroyUi(player, LayerMain);
CuiElementContainer container = new();
// Основная панель с размытым фоном и градиентом
_ = container.Add(
new CuiPanel
{
CursorEnabled = true,
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Image =
{
Color = "0.235 0.227 0.204 0.95",
Material = "assets/content/ui/uibackgroundblur-ingame.mat",
},
},
"Overlay",
LayerMain
);
// Добавляем стильный градиентный фон
_ = container.Add(
new CuiPanel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Image =
{
Color = "0.235 0.227 0.204 0.7",
Sprite = "assets/content/ui/ui.background.transparent.radial.psd",
},
},
LayerMain
);
// Центральная панель с тенью
_ = container.Add(
new CuiPanel
{
RectTransform =
{
AnchorMin = "0.5 0.5",
AnchorMax = "0.5 0.5",
OffsetMin = "-500 -350",
OffsetMax = "500 350",
},
Image = { Color = "0.322 0.306 0.286 0" },
},
LayerMain,
LayerContent
);
// Стильный заголовок с информацией о вайпе
_ = container.Add(
new CuiPanel
{
RectTransform = { AnchorMin = "0 0.92", AnchorMax = "1 1" },
Image =
{
Color = "0.235 0.227 0.204 0.95",
Material = "assets/content/ui/uibackgroundblur-ingame.mat",
},
},
LayerContent,
"Header"
);
// Добавляем статистику сервера
int totalAdmins = storedData.AdminStatistics.Count;
int onlineAdmins = storedData.AdminStatistics.Count(x =>
BasePlayer.FindByID(x.Key)?.IsConnected == true
);
int afkAdmins = activeAdmins.Count(x => x.Value.IsAfk);
_ = container.Add(
new CuiLabel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Text =
{
Text =
"<size=24><color=#e67e22>⚡ Мониторинг администраторов ⚡</color></size>\n"
+ $"<size=14>Вайп начался: <color=#3498db>{storedData.CurrentWipeStart:dd.MM.yyyy HH:mm}</color> | "
+ $"Админов онлайн: <color=#2ecc71>{onlineAdmins}</color>/<color=#95a5a6>{totalAdmins}</color> | "
+ $"AFK: <color=#e74c3c>{afkAdmins}</color></size>",
Font = "robotocondensed-bold.ttf",
Align = TextAnchor.MiddleCenter,
},
},
"Header"
);
// Панель фильтров с улучшенным дизайном
_ = container.Add(
new CuiPanel
{
RectTransform = { AnchorMin = "0.05 0.85", AnchorMax = "0.95 0.91" },
Image =
{
Color = "0.15 0.15 0.15 0.98",
Material = "assets/content/ui/uibackgroundblur-ingame.mat",
},
},
LayerContent,
"FilterPanel"
);
// Улучшенные кнопки фильтров с иконками
AddFilterButton(
container,
"FilterPanel",
"0.02 0.2",
"0.15 0.8",
"🔍 Все",
FilterAll,
player.userID.ToString()
);
AddFilterButton(
container,
"FilterPanel",
"0.16 0.2",
"0.29 0.8",
"🟢 Онлайн",
FilterOnline,
player.userID.ToString()
);
AddFilterButton(
container,
"FilterPanel",
"0.30 0.2",
"0.43 0.8",
"⭕ Оффлайн",
FilterOffline,
player.userID.ToString()
);
AddFilterButton(
container,
"FilterPanel",
"0.44 0.2",
"0.57 0.8",
"💤 AFK",
FilterAfk,
player.userID.ToString()
);
// Кнопки сортировки с иконками
AddFilterButton(
container,
"FilterPanel",
"0.65 0.2",
"0.78 0.8",
"⏱ По активности",
SortTimeActive,
player.userID.ToString()
);
AddFilterButton(
container,
"FilterPanel",
"0.79 0.2",
"0.92 0.8",
"⌛ По AFK",
SortTimeAfk,
player.userID.ToString()
);
// Получаем отфильтрованный список администраторов
IEnumerable<KeyValuePair<ulong, AdminStats>> filteredAdmins = GetFilteredAdmins(
player.userID.ToString(),
storedData.AdminStatistics
);
// Список администраторов с улучшенными карточками
float yOffset = 0.83f;
foreach (KeyValuePair<ulong, AdminStats> admin in filteredAdmins)
{
BasePlayer? adminPlayer = BasePlayer.FindByID(admin.Key);
bool isOnline = adminPlayer?.IsConnected == true;
_ = activeAdmins.TryGetValue(admin.Key, out AdminActivity? currentActivity);
string cardName = $"AdminCard_{admin.Key}";
_ = container.Add(
new CuiPanel
{
RectTransform =
{
AnchorMin = $"0.05 {yOffset - 0.15}",
AnchorMax = $"0.95 {yOffset}",
},
Image =
{
Color = "0.15 0.15 0.15 0.98",
Material = "assets/content/ui/uibackgroundblur-ingame.mat",
},
},
LayerContent,
cardName
);
// Добавляем подсветку для онлайн статуса
string borderColor = isOnline ? "0.18 0.8 0.44 0.3" : "0.5 0.5 0.5 0.1";
_ = container.Add(
new CuiPanel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Image = { Color = borderColor },
},
cardName,
$"{cardName}_border"
);
// Аватар с улучшенной рамкой
if (ImageLibrary != null && avatarUrls.TryGetValue(admin.Key, out string avatarId))
{
string? avatarImage = ImageLibrary.Call("GetImage", avatarId) as string;
if (!string.IsNullOrEmpty(avatarImage))
{
// Фоновый круг для аватара
_ = container.Add(
new CuiPanel
{
RectTransform = { AnchorMin = "0.02 0.1", AnchorMax = "0.08 0.9" },
Image =
{
Color = "0.2 0.2 0.2 1",
Sprite = "assets/content/ui/ui.circle.psd",
},
},
cardName
);
// Аватар
container.Add(
new CuiElement
{
Parent = cardName,
Components =
{
new CuiRawImageComponent
{
Png = avatarImage,
Color = "1 1 1 1",
},
new CuiRectTransformComponent
{
AnchorMin = "0.02 0.1",
AnchorMax = "0.08 0.9",
},
},
}
);
}
}
// Информация об администраторе с трендами
float currentSessionTime = 0f;
float currentAfkTime = 0f;
if (currentActivity != null)
{
currentSessionTime =
Time.realtimeSinceStartup
- currentActivity.SessionStartTime
- currentActivity.TotalAfkTime;
currentAfkTime = currentActivity.TotalAfkTime;
}
float totalActiveTime =
admin.Value.CurrentWipeActiveTime + (isOnline ? currentSessionTime : 0);
float totalAfkTime =
admin.Value.CurrentWipeAfkTime + (isOnline ? currentAfkTime : 0);
float activePercentage =
(totalActiveTime + totalAfkTime) > 0
? totalActiveTime / (totalActiveTime + totalAfkTime) * 100
: 0;
// Добавляем тренд активности
string trendIcon = GetActivityTrendIcon(admin.Value);
string trendColor = GetActivityTrendColor(admin.Value);
_ = container.Add(
new CuiLabel
{
RectTransform = { AnchorMin = "0.1 0.5", AnchorMax = "0.4 0.9" },
Text =
{
Text =
$"<size=16>{admin.Value.LastName}</size>\n<size=12><color={trendColor}>{trendIcon}</color></size>",
Font = "robotocondensed-bold.ttf",
Align = TextAnchor.MiddleLeft,
},
// ... [Code preview truncated due to length. Full source in attached file] ...