C#:
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using System.Runtime.InteropServices;
using System.Xml;
namespace Oxide.Plugins
{
[Info("Stat", "Author", "1.0.0")]
[Description("Плагин для отслеживания статистики игроков")]
internal sealed class Stat : RustPlugin
{
[PluginReference] private Plugin? ImageLibrary;
private Dictionary<ulong, PlayerStats> playerStats = new();
internal PluginConfig config = null!;
private readonly Dictionary<string, string> cachedUIs = new();
internal Timer broadcastTimer = null!;
private const string PERMISSION_USE = "stat.use";
private const string UI_MAIN_PANEL = "Stat_MainPanel";
private const string UI_RIGHT_PANEL = "Stat_RightPanel";
private const string UI_PLAYER_LIST = "Stat_PlayerList";
private const string UI_PAGE_INDICATOR = "Stat_PageIndicator";
/// <summary>
/// Константы для WinAPI
/// </summary>
private const int WS_EX_COMPOSITED = 0x02000000;
/// <summary>
/// Для предотвращения мерцания UI
/// </summary>
[DllImport("user32.dll")]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
private const int GWL_EXSTYLE = -20;
#region Variables
/// <summary>
/// Переменные для работы с аватарами Steam
/// </summary>
private readonly Dictionary<ulong, string> avatarUrls = new();
private const string defaultAvatarUrl = "https://avatars.akamai.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg";
private bool isImageLibraryLoaded;
/// <summary>
/// Словарь для хранения текущей страницы для каждого игрока
/// </summary>
private readonly Dictionary<ulong, int> playerCurrentPage = new();
/// <summary>
/// Константа игроков на странице
/// </summary>
private const int PLAYERS_PER_PAGE = 10;
#endregion Variables
#region Classes
private sealed class PlayerStats
{
public string Name { get; set; } = string.Empty;
public int Points { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public int Suicides { get; set; }
public int ResourcesGathered { get; set; }
public int HelicoptersDestroyed { get; set; }
public int TanksDestroyed { get; set; }
public DateTime LastSeen { get; set; }
/// <summary>
/// Ресурсы по типам
/// </summary>
public int WoodGathered { get; set; }
public int StoneGathered { get; set; }
public int MetalOreGathered { get; set; }
public int SulfurOreGathered { get; set; }
public int BarrelsDestroyed { get; set; }
public int CratesLooted { get; set; }
}
internal sealed class PluginConfig
{
[JsonProperty("Команда для открытия топа")]
public required string TopCommand { get; set; }
[JsonProperty("Настройка начисления очков за добычу")]
public required GatheringPointsConfig GatheringPoints { get; set; }
[JsonProperty("Настройка начисления очков за уничтожение")]
public required DestructionPointsConfig DestructionPoints { get; set; }
[JsonProperty("Настройка начисления и отнимания очков за убийства и смерти")]
public required KillDeathPointsConfig KillDeathPoints { get; set; }
[JsonProperty("Настройка призов")]
public required RewardsConfig Rewards { get; set; }
[JsonProperty("Настройка оповещений в чате")]
public required ChatNotificationsConfig ChatNotifications { get; set; }
[JsonProperty("Config version")]
public required string Version { get; set; }
[JsonProperty("ИД магазина в сервисе")]
public required string ShopId { get; set; }
[JsonProperty("Секретный ключ (не распростраяйте его)")]
public required string SecretKey { get; set; }
[JsonProperty("Место в топе и выдаваемый баланс игроку")]
public required Dictionary<int, string> RewardsByRank { get; set; }
public sealed class GatheringPointsConfig
{
[JsonProperty("Сколько давать очков за дерево")]
public int WoodPoints { get; set; }
[JsonProperty("Сколько давать очков за каменный камень")]
public int StonePoints { get; set; }
[JsonProperty("Сколько давать очков за металический камень")]
public int MetalOrePoints { get; set; }
[JsonProperty("Сколько давать очков за серный камень")]
public int SulfurPoints { get; set; }
[JsonProperty("Сколько давать очков за уничтожение бочки | Лутание обычного ящика у дороги")]
public int BarrelCratePoints { get; set; }
}
public sealed class DestructionPointsConfig
{
[JsonProperty("Сколько давать очков за уничтожение вертолета")]
public int HelicopterPoints { get; set; }
[JsonProperty("Сколько давать очков за уничтожение танка")]
public int TankPoints { get; set; }
}
public sealed class KillDeathPointsConfig
{
[JsonProperty("Сколько давать очков за убийство игрока")]
public int KillPoints { get; set; }
[JsonProperty("Сколько давать очков за убийство NPC (бота)")]
public int NPCKillPoints { get; set; }
[JsonProperty("Сколько отнимать очков за смерть")]
public int DeathPoints { get; set; }
[JsonProperty("Сколько отнимать очков за суицид")]
public int SuicidePoints { get; set; }
}
public sealed class RewardsConfig
{
[JsonProperty("Включить авто выдачу призов при вайпе сервера?")]
public bool EnableAutoRewards { get; set; }
[JsonProperty("ИД магазина в сервисе")]
public required string ShopId { get; set; }
[JsonProperty("Секретный ключ (не распростраяйте его)")]
public required string SecretKey { get; set; }
[JsonProperty("Место в топе и выдаваемый баланс игроку")]
public required Dictionary<int, string> RewardsByRank { get; set; }
}
public sealed class ChatNotificationsConfig
{
[JsonProperty("Отправлять в чат сообщения с топ 5 игроками ?")]
public bool EnableTopPlayersBroadcast { get; set; }
[JsonProperty("Раз в сколько секунд будет отправлятся сообщение ?")]
public int BroadcastInterval { get; set; }
}
}
#endregion Classes
#region Config
protected override void LoadDefaultConfig()
{
config = new PluginConfig
{
TopCommand = "top",
GatheringPoints = new PluginConfig.GatheringPointsConfig
{
WoodPoints = 5,
StonePoints = 5,
MetalOrePoints = 5,
SulfurPoints = 5,
BarrelCratePoints = 5
},
DestructionPoints = new PluginConfig.DestructionPointsConfig
{
HelicopterPoints = 1500,
TankPoints = 750
},
KillDeathPoints = new PluginConfig.KillDeathPointsConfig
{
KillPoints = 40,
NPCKillPoints = 20,
DeathPoints = 15,
SuicidePoints = 15
},
Rewards = new PluginConfig.RewardsConfig
{
EnableAutoRewards = true,
ShopId = "",
SecretKey = "",
RewardsByRank = new Dictionary<int, string>
{
{ 1, "400.0" },
{ 2, "250.0" },
{ 3, "150.0" },
{ 4, "100.0" },
{ 5, "50.0" },
{ 6, "50.0" },
{ 7, "30.0" }
}
},
ChatNotifications = new PluginConfig.ChatNotificationsConfig
{
EnableTopPlayersBroadcast = true,
BroadcastInterval = 1200
},
Version = "2.0.0",
ShopId = "",
SecretKey = "",
RewardsByRank = new Dictionary<int, string>
{
{ 1, "400.0" },
{ 2, "250.0" },
{ 3, "150.0" },
{ 4, "100.0" },
{ 5, "50.0" },
{ 6, "50.0" },
{ 7, "30.0" }
}
};
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null)
{
LoadDefaultConfig();
}
}
catch
{
LoadDefaultConfig();
}
SaveConfig();
}
protected override void SaveConfig()
{
Config.WriteObject(config);
}
#endregion Config
#region Oxide Hooks
private void Init()
{
permission.RegisterPermission(PERMISSION_USE, this);
cmd.AddChatCommand(config.TopCommand, this, nameof(CmdOpenTop));
LoadData();
if (config.ChatNotifications.EnableTopPlayersBroadcast)
{
StartBroadcastTimer();
}
}
private void OnServerInitialized()
{
if (ImageLibrary == null)
{
PrintError("ImageLibrary plugin is not installed!");
}
// Проверяем наличие ImageLibrary
Plugin? imageLibrary = plugins.Find("ImageLibrary");
isImageLibraryLoaded = imageLibrary != null;
if (!isImageLibraryLoaded)
{
PrintError("ImageLibrary не обнаружена! Аватарки не будут загружены.");
}
else
{
// Инициализируем ImageLibrary
ImageLibrary = imageLibrary;
PrintWarning("ImageLibrary обнаружена. Инициализация аватарок...");
// Предзагружаем стандартную аватарку
if (ImageLibrary != null)
{
_ = ImageLibrary.Call("AddImage", defaultAvatarUrl, "default_avatar", 0UL);
PrintWarning("Стандартная аватарка загружена");
}
// Предзагружаем аватарки для существующих игроков
foreach (BasePlayer player in BasePlayer.activePlayerList)
{
if (player?.IsNpc == false)
{
GetPlayerAvatar(player.userID, (avatarKey) => { });
}
}
}
}
private void Unload()
{
SaveData();
CloseAllUIs();
broadcastTimer?.Destroy();
}
private void OnServerSave()
{
SaveData();
}
private void OnDispenserGather(ResourceDispenser dispenser, BaseEntity entity, Item item)
{
if (entity is not BasePlayer player || item == null)
{
return;
}
int points;
switch (item.info.shortname)
{
case "wood":
points = config.GatheringPoints.WoodPoints;
GetOrCreatePlayerStats(player.userID).WoodGathered += item.amount;
break;
case "stones":
points = config.GatheringPoints.StonePoints;
GetOrCreatePlayerStats(player.userID).StoneGathered += item.amount;
break;
case "metal.ore":
points = config.GatheringPoints.MetalOrePoints;
GetOrCreatePlayerStats(player.userID).MetalOreGathered += item.amount;
break;
case "sulfur.ore":
points = config.GatheringPoints.SulfurPoints;
GetOrCreatePlayerStats(player.userID).SulfurOreGathered += item.amount;
break;
default:
return;
}
AddPointsToPlayer(player.userID, points);
GetOrCreatePlayerStats(player.userID).ResourcesGathered++;
}
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
if (entity == null || info == null)
{
return;
}
// Проверка на уничтожение бочки/ящика
if (entity.ShortPrefabName.Contains("barrel") && info.Initiator is BasePlayer barrelPlayer)
{
AddPointsToPlayer(barrelPlayer.userID, config.GatheringPoints.BarrelCratePoints);
GetOrCreatePlayerStats(barrelPlayer.userID).BarrelsDestroyed++;
return;
}
if (entity.ShortPrefabName.Contains("crate_basic") && info.Initiator is BasePlayer cratePlayer)
{
AddPointsToPlayer(cratePlayer.userID, config.GatheringPoints.BarrelCratePoints);
GetOrCreatePlayerStats(cratePlayer.userID).CratesLooted++;
return;
}
// Проверка на уничтожение вертолета
if (entity is PatrolHelicopter && info.Initiator is BasePlayer helicopterKiller)
{
AddPointsToPlayer(helicopterKiller.userID, config.DestructionPoints.HelicopterPoints);
GetOrCreatePlayerStats(helicopterKiller.userID).HelicoptersDestroyed++;
return;
}
// Проверка на уничтожение танка
if (entity is BradleyAPC && info.Initiator is BasePlayer tankKiller)
{
AddPointsToPlayer(tankKiller.userID, config.DestructionPoints.TankPoints);
GetOrCreatePlayerStats(tankKiller.userID).TanksDestroyed++;
return;
}
// Проверка на убийство NPC (бота)
if (entity is BasePlayer victim && !victim.userID.IsSteamId() && info.Initiator is BasePlayer npcKiller && npcKiller.userID.IsSteamId())
{
AddPointsToPlayer(npcKiller.userID, config.KillDeathPoints.NPCKillPoints);
return;
}
// Проверка на убийство игрока
if (entity is BasePlayer playerVictim && playerVictim.userID.IsSteamId() && info.Initiator != null)
{
// Отнимаем очки за смерть
SubtractPointsFromPlayer(playerVictim.userID, config.KillDeathPoints.DeathPoints);
GetOrCreatePlayerStats(playerVictim.userID).Deaths++;
// Проверка на суицид
if (info.Initiator == playerVictim)
{
SubtractPointsFromPlayer(playerVictim.userID, config.KillDeathPoints.SuicidePoints);
GetOrCreatePlayerStats(playerVictim.userID).Suicides++;
return;
}
// Проверка на убийство игроком
if (info.Initiator is BasePlayer killer && killer.userID.IsSteamId())
{
AddPointsToPlayer(killer.userID, config.KillDeathPoints.KillPoints);
GetOrCreatePlayerStats(killer.userID).Kills++;
}
}
}
private void OnPlayerConnected(BasePlayer player)
{
if (player?.userID.IsSteamId() != true)
{
return;
}
PlayerStats stats = GetOrCreatePlayerStats(player.userID);
stats.Name = player.displayName;
stats.LastSeen = DateTime.Now;
// Загружаем аватарку игрока при подключении
if (isImageLibraryLoaded && ImageLibrary != null)
{
GetPlayerAvatar(player.userID, (avatarKey) => { });
}
}
private void OnPlayerDisconnected(BasePlayer player)
{
if (player?.userID.IsSteamId() != true)
{
return;
}
PlayerStats stats = GetOrCreatePlayerStats(player.userID);
stats.LastSeen = DateTime.Now;
DestroyUI(player, UI_MAIN_PANEL);
}
private void OnServerWipe()
{
if (config.Rewards.EnableAutoRewards)
{
DistributeRewards();
}
// Очищаем статистику после выдачи призов
playerStats.Clear();
SaveData();
}
#endregion Oxide Hooks
#region Commands
private void CmdOpenTop(BasePlayer player, string command, string[] args)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, PERMISSION_USE))
{
return;
}
// Показываем UI с общей статистикой и статистикой игрока справа
DisplayTopUI(player, player.userID);
}
[ChatCommand("mystats")]
private void CmdChatMyStats(BasePlayer player, string command, string[] args)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, PERMISSION_USE))
{
return;
}
// Показываем UI с личной статистикой игрока
DisplayTopUI(player, player.userID);
}
[ConsoleCommand("stat.mystats")]
private void CmdMyStats(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player == null || !permission.UserHasPermission(player.UserIDString, PERMISSION_USE))
{
return;
}
// Показываем UI с личной статистикой игрока
DisplayTopUI(player, player.userID);
}
#endregion Commands
#region UI
/// <summary>
/// Метод для включения композитного режима окна (устраняет мерцание)
/// </summary>
private void EnableCompositedUI(BasePlayer player)
{
// Этот код будет выполняться на клиенте для применения стиля WS_EX_COMPOSITED
const string compositingCode = @"
try {
var mainWindow = UnityEngine.Display.main.GetGameView();
if (mainWindow != null) {
var hwnd = mainWindow.GetNativeWindowHandle();
if (hwnd != System.IntPtr.Zero) {
int style = GetWindowLong(hwnd, -20);
SetWindowLong(hwnd, -20, style | 0x02000000);
}
}
} catch (Exception ex) {
// Игнорируем ошибки
}
";
// Отправляем код для выполнения на клиенте
player.SendConsoleCommand("clientrun", compositingCode);
}
private void DisplayTopUI(BasePlayer player, ulong selectedPlayerId = 0)
{
if (player == null)
{
return;
}
// Включаем композитный режим, чтобы избежать мерцания
EnableCompositedUI(player);
// Проверяем, есть ли у игрока текущая страница, если нет - устанавливаем 1
if (!playerCurrentPage.ContainsKey(player.userID))
{
playerCurrentPage[player.userID] = 1;
}
// Получаем текущую страницу для игрока
int currentPage = playerCurrentPage[player.userID];
// Удаляем предыдущий UI если он был
DestroyUI(player, UI_MAIN_PANEL);
// Создаем контейнер для UI с композитным режимом
CuiElementContainer container = new()
{
// Создаем главную панель с темным фоном и свойством предотвращения мерцания
{
new CuiPanel
{
RectTransform = { AnchorMin = "0.2 0.15", AnchorMax = "0.8 0.9" }, // Увеличиваем размер для лучшей видимости
Image = { Color = "0.1 0.11 0.15 0.97", Material = "assets/content/ui/uibackgroundblur.mat" }, // Более темный фон для контраста
CursorEnabled = true
},
"Overlay",
UI_MAIN_PANEL
}
};
// Добавляем небольшую декоративную верхнюю панель
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.95", AnchorMax = "1 1" },
Image = { Color = "0.15 0.40 0.50 1", Material = "assets/content/ui/uibackgroundblur-ingamemenu.mat" }
}, UI_MAIN_PANEL, "HeaderBar");
// Улучшенный заголовок "ТОП ИГРОКОВ" с тенью
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.01 0.951", AnchorMax = "0.99 0.999" },
Text = { Text = "ТОП ИГРОКОВ", Font = "robotocondensed-bold.ttf", FontSize = 24, Align = TextAnchor.MiddleCenter, Color = "0 0 0 0.5" } // Тень текста
}, UI_MAIN_PANEL);
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0.95", AnchorMax = "1 1" },
Text = { Text = "ТОП ИГРОКОВ", Font = "robotocondensed-bold.ttf", FontSize = 24, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, UI_MAIN_PANEL);
// Кнопка "ЗАКРЫТЬ" в правом верхнем углу с улучшенным стилем
_ = container.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.92 0.95", AnchorMax = "0.99 0.99" },
Button = {
Color = "0.7 0.2 0.2 0.8",
Command = "stat.close",
Material = "assets/content/ui/uibackgroundblur-ingamemenu.mat"
},
Text = { Text = "✕", Font = "robotocondensed-bold.ttf", FontSize = 20, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, UI_MAIN_PANEL);
// Основной контейнер для контента
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.01 0.01", AnchorMax = "0.99 0.94" },
Image = { Color = "0 0 0 0" }
}, UI_MAIN_PANEL, "ContentContainer");
// Правая панель с улучшенным стилем (для статистики игрока)
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.56 0.02", AnchorMax = "0.98 0.93" },
Image = {
Color = "0.12 0.15 0.18 0.95",
Material = "assets/content/ui/uibackgroundblur.mat"
}
}, "ContentContainer", "RightPanel");
// Стильный заголовок правой панели
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.93", AnchorMax = "1 1" },
Image = { Color = "0.15 0.4 0.3 1" }
}, "RightPanel", "RightPanelHeader");
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0.93", AnchorMax = "1 1" },
Text = {
Text = "ИНФОРМАЦИЯ ИГРОКА",
Font = "robotocondensed-bold.ttf",
FontSize = 16,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1"
}
}, "RightPanel");
// Левая панель с улучшенным стилем (список игроков)
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.02 0.02", AnchorMax = "0.54 0.93" },
Image = {
Color = "0.12 0.15 0.18 0.95",
Material = "assets/content/ui/uibackgroundblur.mat"
}
}, "ContentContainer", "LeftPanel");
// Стильный заголовок левой панели
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.93", AnchorMax = "1 1" },
Image = { Color = "0.15 0.4 0.3 1" }
}, "LeftPanel", "LeftPanelHeader");
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0.93", AnchorMax = "1 1" },
Text = {
Text = "СПИСОК ИГРОКОВ",
Font = "robotocondensed-bold.ttf",
FontSize = 16,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1"
}
}, "LeftPanel");
// Улучшенные заголовки таблицы в левой панели с подсветкой фона
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.86", AnchorMax = "1 0.93" },
Image = { Color = "0.15 0.2 0.25 0.9" }
}, "LeftPanel", "TableHeader");
// Заголовки таблицы с улучшенным форматированием
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.02 0.86", AnchorMax = "0.15 0.93" },
Text = { Text = "МЕСТО", Font = "robotocondensed-bold.ttf", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 0.85 0.4 1" }
}, "LeftPanel");
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.16 0.86", AnchorMax = "0.45 0.93" },
Text = { Text = "ИГРОК", Font = "robotocondensed-bold.ttf", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "1 0.85 0.4 1" }
}, "LeftPanel");
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.46 0.86", AnchorMax = "0.65 0.93" },
Text = { Text = "СТАТУС", Font = "robotocondensed-bold.ttf", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 0.85 0.4 1" }
}, "LeftPanel");
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.66 0.86", AnchorMax = "0.8 0.93" },
Text = { Text = "ОЧКИ", Font = "robotocondensed-bold.ttf", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 0.85 0.4 1" }
}, "LeftPanel");
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.81 0.86", AnchorMax = "0.98 0.93" },
Text = { Text = "НАГРАДА", Font = "robotocondensed-bold.ttf", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 0.85 0.4 1" }
}, "LeftPanel");
// Контейнер для списка игроков
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0.07", AnchorMax = "1 0.86" },
Image = { Color = "0 0 0 0" }
}, "LeftPanel", "PlayerListContainer");
// Получаем всех игроков, отсортированных по очкам
List<KeyValuePair<ulong, PlayerStats>> allPlayersSorted = playerStats
.OrderByDescending(p => p.Value.Points)
.ToList();
// Вычисляем общее количество страниц
int totalPages = (int)Math.Ceiling(allPlayersSorted.Count / (float)PLAYERS_PER_PAGE);
if (totalPages < 1)
{
totalPages = 1;
}
// Получаем игроков для текущей страницы
List<KeyValuePair<ulong, PlayerStats>> pagePlayersWithIds = allPlayersSorted
.Skip((currentPage - 1) * PLAYERS_PER_PAGE)
.Take(PLAYERS_PER_PAGE)
.ToList();
// Выводим строки с игроками с улучшенным стилем
for (int i = 0; i < pagePlayersWithIds.Count; i++)
{
ulong playerId = pagePlayersWithIds[i].Key;
PlayerStats playerStat = pagePlayersWithIds[i].Value;
// Улучшенное распределение строк для равномерного отображения
const float rowHeight = 0.79f / PLAYERS_PER_PAGE;
float yMin = 1f - ((i + 1) * rowHeight);
float yMax = 1f - (i * rowHeight);
// Более четкое чередование цветов строк с улучшенным контрастом
string rowColor = i % 2 == 0 ? "0.14 0.17 0.22 0.7" : "0.16 0.19 0.24 0.7";
// Выделение выбранного игрока с более ярким цветом
string rowHighlight = playerId == selectedPlayerId ? "0.2 0.35 0.3 0.9" : rowColor;
// Вычисляем глобальный ранг игрока
int globalRank = allPlayersSorted.FindIndex(p => p.Key == playerId) + 1;
// Фон строки с закругленными углами для лучшего визуального восприятия
_ = container.Add(new CuiPanel
{
RectTransform = {
AnchorMin = $"0.01 {yMin}",
AnchorMax = $"0.99 {yMax - 0.005f}" // Небольшой отступ между строками
},
Image = { Color = rowHighlight }
}, "PlayerListContainer", $"PlayerRow_{playerId}");
// Место с выделением цветом по позиции
string rankColor = globalRank <= 3 ? GetRankColor(globalRank) : "1 1 1 1";
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.02 0", AnchorMax = "0.15 1" },
Text = {
Text = $"#{globalRank}",
Font = "robotocondensed-bold.ttf",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = rankColor
}
}, $"PlayerRow_{playerId}");
// Имя игрока с улучшенным стилем
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.16 0", AnchorMax = "0.45 1" },
Text = {
Text = playerStat.Name,
Font = globalRank <= 3 ? "robotocondensed-bold.ttf" : "robotocondensed-regular.ttf",
FontSize = 14,
Align = TextAnchor.MiddleLeft,
Color = globalRank <= 3 ? GetRankColor(globalRank) : "1 1 1 1"
}
}, $"PlayerRow_{playerId}");
// Статус с более выразительными цветами
bool isOnline = BasePlayer.activePlayerList.Any(p => p.displayName == playerStat.Name);
string statusText = isOnline ? "Online" : "Offline";
string statusColor = isOnline ? "0.2 1 0.4 1" : "0.7 0.7 0.7 0.7";
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.46 0", AnchorMax = "0.65 1" },
Text = {
Text = statusText,
Font = "robotocondensed-regular.ttf",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = statusColor
}
}, $"PlayerRow_{playerId}");
// Очки с выделением для лидеров
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.66 0", AnchorMax = "0.8 1" },
Text = {
Text = playerStat.Points.ToString(),
Font = globalRank <= 3 ? "robotocondensed-bold.ttf" : "robotocondensed-regular.ttf",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = globalRank <= 3 ? GetRankColor(globalRank) : "1 1 1 1"
}
}, $"PlayerRow_{playerId}");
// Награда с улучшенным отображением
string reward = "—";
if (globalRank <= 7 && config.Rewards.RewardsByRank.TryGetValue(globalRank, out string rewardAmount))
{
reward = rewardAmount;
}
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.81 0", AnchorMax = "0.98 1" },
Text = {
Text = reward,
Font = "robotocondensed-bold.ttf",
FontSize = 14,
Align = TextAnchor.MiddleCenter,
Color = globalRank <= 7 ? "1 0.9 0.3 1" : "0.7 0.7 0.7 0.5"
}
}, $"PlayerRow_{playerId}");
// Добавляем кнопку для выбора игрока
_ = container.Add(new CuiButton
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Button = {
Color = "0 0 0 0",
Command = $"stat.selectplayer {playerId}"
},
Text = { Text = "" }
}, $"PlayerRow_{playerId}");
}
// Контейнер для кнопок пагинации
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.25 0.01", AnchorMax = "0.75 0.06" },
Image = { Color = "0.15 0.2 0.25 0.8" }
}, "LeftPanel", "PaginationContainer");
// Улучшенные кнопки пагинации
_ = container.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.25 0", AnchorMax = "0.4 1" },
Button = {
Color = currentPage > 1 ? "0.2 0.5 0.7 0.9" : "0.2 0.2 0.3 0.5",
Command = "stat.prevpage"
},
Text = {
Text = "◀",
Font = "robotocondensed-bold.ttf",
FontSize = 18,
Align = TextAnchor.MiddleCenter,
Color = currentPage > 1 ? "1 1 1 1" : "0.5 0.5 0.5 0.5"
}
}, "PaginationContainer");
// Индикатор текущей страницы с более стильным видом
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.4 0", AnchorMax = "0.6 1" },
Text = {
Text = $"{currentPage}/{totalPages}",
Font = "robotocondensed-regular.ttf",
FontSize = 16,
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1"
}
}, "PaginationContainer");
_ = container.Add(new CuiButton
{
RectTransform = { AnchorMin = "0.6 0", AnchorMax = "0.75 1" },
Button = {
Color = currentPage < totalPages ? "0.2 0.5 0.7 0.9" : "0.2 0.2 0.3 0.5",
Command = "stat.nextpage"
},
Text = {
Text = "▶",
Font = "robotocondensed-bold.ttf",
FontSize = 18,
Align = TextAnchor.MiddleCenter,
Color = currentPage < totalPages ? "1 1 1 1" : "0.5 0.5 0.5 0.5"
}
}, "PaginationContainer");
// Добавляем кнопку МОЯ СТАТИСТИКА с улучшенным стилем (уменьшенная)
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.69 0.02", AnchorMax = "0.85 0.07" }, // Делаем кнопку еще меньше и немного сдвигаем вправо
Image = { Color = "0.15 0.5 0.25 0.9" }
}, "ContentContainer", "MyStatsButtonContainer");
_ = container.Add(new CuiButton
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Button = {
Color = "0 0 0 0",
Command = $"stat.selectplayer {player.userID}"
},
Text = {
Text = "МОЯ СТАТИСТИКА",
Font = "robotocondensed-bold.ttf",
FontSize = 14, // Уменьшаем размер шрифта
Align = TextAnchor.MiddleCenter,
Color = "1 1 1 1"
}
}, "MyStatsButtonContainer");
// Показываем статистику выбранного игрока или текущего игрока в правой панели
ulong playerIdToShow = selectedPlayerId > 0 ? selectedPlayerId : player.userID;
DisplayPlayerStats(container, playerIdToShow);
// Применяем UI
BasePlayer basePlayer = BasePlayer.FindByID(player.userID);
if (basePlayer != null)
{
_ = CuiHelper.AddUi(basePlayer, container);
}
}
/// <summary>
/// Функция для получения цвета ранга (для топ-3 игроков)
/// </summary>
private string GetRankColor(int rank)
{
return rank switch
{
1 => "1 0.9 0.2 1",// Золото
2 => "0.9 0.9 0.9 1",// Серебро
3 => "0.8 0.5 0.2 1",// Бронза
_ => "1 1 1 1",// Обычный цвет
};
}
private void DisplayPlayerStats(CuiElementContainer container, ulong playerId)
{
if (!playerStats.TryGetValue(playerId, out PlayerStats? stats))
{
// Если статистики нет, выводим сообщение
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.1 0.4", AnchorMax = "0.9 0.6" },
Text = { Text = "Нет данных для этого игрока", Font = "robotocondensed-regular.ttf", FontSize = 16, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, "RightPanel");
return;
}
// Очищаем предыдущее содержимое правой панели
_ = CuiHelper.DestroyUi(BasePlayer.FindByID(playerId), "RightPanelContent");
// Создаем новый контейнер для содержимого правой панели
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Image = { Color = "0.12 0.22 0.18 0.95", Material = "assets/content/ui/uibackgroundblur.mat" }
}, "RightPanel", "RightPanelContent");
// Имя игрока в верхней части
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0.05 0.92", AnchorMax = "0.95 1" },
Text = { Text = stats.Name, Font = "robotocondensed-bold.ttf", FontSize = 22, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, "RightPanelContent");
// Проверяем, есть ли уже аватарка игрока в ImageLibrary
string steamAvatarKey = $"steam_avatar_{playerId}";
if (ImageLibrary?.Call<bool>("HasImage", steamAvatarKey, 0UL) == true)
{
// Если аватарка уже загружена, сразу отображаем
string avatarImage = ImageLibrary.Call<string>("GetImage", steamAvatarKey, 0UL);
// Добавляем элемент с изображением напрямую
container.Add(new CuiElement
{
Parent = "RightPanelContent",
Name = "AvatarImage",
Components =
{
new CuiRawImageComponent { Png = avatarImage, Color = "1 1 1 1" },
new CuiRectTransformComponent { AnchorMin = "0.35 0.75", AnchorMax = "0.65 0.9" }
}
});
PrintWarning($"Аватарка игрока {playerId} отображена напрямую");
}
else
{
// Если аватарки нет, создаем фоновую панель
_ = container.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.35 0.75", AnchorMax = "0.65 0.9" },
Image = { Color = "0.1 0.1 0.1 0.5" }
}, "RightPanelContent", "AvatarBackground");
// Убираем временную надпись "Загрузка", оставляем только фон
// Запускаем асинхронную загрузку аватарки
ulong currentPlayerId = playerId; // Создаем копию переменной для использования в лямбда-выражении
GetPlayerAvatar(currentPlayerId, (finalAvatarKey) =>
{
BasePlayer targetPlayer = BasePlayer.FindByID(currentPlayerId);
if (targetPlayer?.IsConnected != true)
{
return;
}
// Создаем новый контейнер для обновления UI
CuiElementContainer updateContainer = new()
{
// Удаляем старую панель с фоном
new CuiElement
{
Name = "AvatarBackground",
DestroyUi = "AvatarBackground"
}
};
if (ImageLibrary?.Call<bool>("HasImage", finalAvatarKey, 0UL) ?? false)
{
// Добавляем элемент с аватаркой
updateContainer.Add(new CuiElement
{
Parent = "RightPanelContent",
Name = "AvatarImage",
Components =
{
new CuiRawImageComponent { Png = ImageLibrary.Call<string>("GetImage", finalAvatarKey, 0UL), Color = "1 1 1 1" },
new CuiRectTransformComponent { AnchorMin = "0.35 0.75", AnchorMax = "0.65 0.9" }
}
});
PrintWarning($"Аватарка игрока {currentPlayerId} загружена и отображена");
}
else
{
// Если аватарку не удалось загрузить, показываем сообщение об ошибке
string errorPanel = updateContainer.Add(new CuiPanel
{
RectTransform = { AnchorMin = "0.35 0.75", AnchorMax = "0.65 0.9" },
Image = { Color = "0.1 0.1 0.1 0.5" }
}, "RightPanelContent", "AvatarError");
_ = updateContainer.Add(new CuiLabel
{
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" },
Text = { Text = "Нет\nаватара", Font = "robotocondensed-bold.ttf", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, errorPanel);
PrintWarning($"Не удалось загрузить аватарку для игрока {currentPlayerId}");
}
// Отправляем обновленный UI игроку
_ = CuiHelper.AddUi(targetPlayer, updateContainer);
});
}
// Левая сторона статистики (метки)
const float leftLabelWidth = 0.45f;
const float labelHeight = 0.04f;
const float startY = 0.85f;
const float labelSpacing = 0.05f;
// МЕСТО В ТОПЕ
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"0.01 {startY}", AnchorMax = $"{leftLabelWidth} {startY + labelHeight}" },
Text = { Text = "МЕСТО В ТОПЕ:", Font = "robotocondensed-regular.ttf", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "0.8 0.8 0.8 1" }
}, "RightPanelContent");
// ОЧКОВ
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"0.01 {startY - labelSpacing}", AnchorMax = $"{leftLabelWidth} {startY - labelSpacing + labelHeight}" },
Text = { Text = "ОЧКОВ:", Font = "robotocondensed-regular.ttf", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "0.8 0.8 0.8 1" }
}, "RightPanelContent");
// АКТИВНОСТЬ
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"0.01 {startY - (labelSpacing * 2)}", AnchorMax = $"{leftLabelWidth} {startY - (labelSpacing * 2) + labelHeight}" },
Text = { Text = "АКТИВНОСТЬ:", Font = "robotocondensed-regular.ttf", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "0.8 0.8 0.8 1" }
}, "RightPanelContent");
// УБИЙСТВ
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"0.01 {startY - (labelSpacing * 3)}", AnchorMax = $"{leftLabelWidth} {startY - (labelSpacing * 3) + labelHeight}" },
Text = { Text = "УБИЙСТВ:", Font = "robotocondensed-regular.ttf", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "0.8 0.8 0.8 1" }
}, "RightPanelContent");
// СМЕРТЕЙ
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"0.01 {startY - (labelSpacing * 4)}", AnchorMax = $"{leftLabelWidth} {startY - (labelSpacing * 4) + labelHeight}" },
Text = { Text = "СМЕРТЕЙ:", Font = "robotocondensed-regular.ttf", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "0.8 0.8 0.8 1" }
}, "RightPanelContent");
// К/Д
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"0.01 {startY - (labelSpacing * 5)}", AnchorMax = $"{leftLabelWidth} {startY - (labelSpacing * 5) + labelHeight}" },
Text = { Text = "К/Д:", Font = "robotocondensed-regular.ttf", FontSize = 14, Align = TextAnchor.MiddleLeft, Color = "0.8 0.8 0.8 1" }
}, "RightPanelContent");
// Правая сторона статистики (значения)
const float rightValueStart = 0.55f;
const float rightValueWidth = 0.95f;
// Значение для МЕСТО В ТОПЕ
int rank = GetPlayerRank(playerId);
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"{rightValueStart} {startY}", AnchorMax = $"{rightValueWidth} {startY + labelHeight}" },
Text = { Text = rank.ToString(), Font = "robotocondensed-bold.ttf", FontSize = 16, Align = TextAnchor.MiddleRight, Color = "1 1 1 1" }
}, "RightPanelContent");
// Значение для ОЧКОВ
_ = container.Add(new CuiLabel
{
RectTransform = { AnchorMin = $"{rightValueStart} {startY - labelSpacing}", AnchorMax = $"{rightValueWidth} {startY - labelSpacing + labelHeight}" },
Text = { Text =
// ... [Code preview truncated due to length. Full source in attached file] ...