/
foreks73
/
Statistics
Обзор
Документация
Войти
/
foreks73
/
Statistics
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Statistics.cs
1 382 строки
56 KB
foreks73
upload files
11 июн 2025, 17:18
11 июн 2025, 17:18
441ad65
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Oxide.Core; using Oxide.Core.Plugins; using Oxide.Game.Rust.Cui; using UnityEngine; using Newtonsoft.Json; namespace Oxide.Plugins { [Info("Statistics", "YourName", "1.0.0")] [Description("Comprehensive player statistics tracking system")] class Statistics : RustPlugin { #region Fields private StoredData storedData; private Configuration config; private Dictionary<ulong, DateTime> playerConnectTimes = new Dictionary<ulong, DateTime>(); private Dictionary<ulong, Timer> uiTimers = new Dictionary<ulong, Timer>(); private const string UIMainName = "StatisticsUI"; #endregion #region Configuration private class Configuration { [JsonProperty("Database File Name")] public string DatabaseFile { get; set; } = "Statistics"; [JsonProperty("Enable Kill Statistics")] public bool EnableKillStats { get; set; } = true; [JsonProperty("Enable Resource Statistics")] public bool EnableResourceStats { get; set; } = true; [JsonProperty("Enable Building Statistics")] public bool EnableBuildingStats { get; set; } = true; [JsonProperty("Show Achievement Notifications")] public bool ShowAchievementNotifications { get; set; } = true; [JsonProperty("Enable Web API")] public bool EnableWebAPI { get; set; } = false; [JsonProperty("Achievement Settings")] public AchievementSettings Achievements { get; set; } = new AchievementSettings(); [JsonProperty("UI Colors")] public UIColors Colors { get; set; } = new UIColors(); } protected override void LoadDefaultConfig() { config = new Configuration(); } protected override void LoadConfig() { base.LoadConfig(); try { config = Config.ReadObject<Configuration>(); if (config == null) { throw new JsonException(); } } catch { PrintWarning("Configuration file is corrupt, creating new one..."); LoadDefaultConfig(); } SaveConfig(); } protected override void SaveConfig() => Config.WriteObject(config); #endregion #region Data Classes public class StoredData { public Dictionary<ulong, PlayerStats> PlayerStatistics = new Dictionary<ulong, PlayerStats>(); public GlobalStats GlobalStatistics = new GlobalStats(); } public class PlayerStats { public string PlayerName { get; set; } = ""; public int Kills { get; set; } = 0; public int Deaths { get; set; } = 0; public int Headshots { get; set; } = 0; public TimeSpan PlayTime { get; set; } = TimeSpan.Zero; public DateTime FirstJoin { get; set; } = DateTime.Now; public DateTime LastSeen { get; set; } = DateTime.Now; public Dictionary<string, int> WeaponKills { get; set; } = new Dictionary<string, int>(); public Dictionary<string, int> ResourcesGathered { get; set; } = new Dictionary<string, int>(); public int BlocksBuilt { get; set; } = 0; public int BlocksDestroyed { get; set; } = 0; public List<string> Achievements { get; set; } = new List<string>(); public Dictionary<string, DailyStats> DailyStatistics { get; set; } = new Dictionary<string, DailyStats>(); public double KDRatio => Deaths > 0 ? Math.Round((double)Kills / Deaths, 2) : Kills; public double HeadshotRatio => Kills > 0 ? Math.Round((double)Headshots / Kills * 100, 1) : 0; public string FavoriteWeapon => WeaponKills.Count > 0 ? WeaponKills.OrderByDescending(x => x.Value).First().Key : "Unknown"; } public class DailyStats { public int Kills { get; set; } = 0; public int Deaths { get; set; } = 0; public int ResourcesGathered { get; set; } = 0; public TimeSpan PlayTime { get; set; } = TimeSpan.Zero; } public class GlobalStats { public int TotalKills { get; set; } = 0; public int TotalDeaths { get; set; } = 0; public TimeSpan TotalPlayTime { get; set; } = TimeSpan.Zero; public int TotalResourcesGathered { get; set; } = 0; public ServerRecords Records { get; set; } = new ServerRecords(); } public class ServerRecords { public RecordEntry LongestKill { get; set; } = new RecordEntry(); } public class RecordEntry { public string PlayerName { get; set; } = ""; public ulong PlayerId { get; set; } = 0; public float Value { get; set; } = 0f; public DateTime Date { get; set; } = DateTime.Now; } public class AchievementSettings { public List<int> KillMilestones { get; set; } = new List<int> { 10, 25, 50, 100, 250, 500, 1000 }; public List<int> PlayTimeMilestones { get; set; } = new List<int> { 10, 24, 48, 100, 200, 500 }; public List<int> ResourceMilestones { get; set; } = new List<int> { 1000, 5000, 10000, 25000, 50000, 100000 }; } public class UIColors { public string Background { get; set; } = "0.1 0.1 0.1 0.95"; public string Panel { get; set; } = "0.15 0.15 0.15 0.9"; public string Button { get; set; } = "0.3 0.3 0.3 1"; public string Text { get; set; } = "1 1 1 1"; } #endregion #region Localization protected override void LoadDefaultMessages() { lang.RegisterMessages(new Dictionary<string, string> { ["NoPermission"] = "У вас нет прав для использования этой команды", ["StatsTitle"] = "Статистика игрока {0}", ["Kills"] = "Убийства: {0}", ["Deaths"] = "Смерти: {0}", ["KDRatio"] = "K/D: {0}", ["PlayTime"] = "Время игры: {0}", ["FirstJoin"] = "Первый вход: {0}", ["StatsNotFound"] = "Статистика не найдена", ["TopKillers"] = "Топ убийц", ["TopPlayTime"] = "Топ по времени игры", ["StatsReset"] = "Статистика сброшена", ["AllStatsReset"] = "Вся статистика сброшена", ["PlayerNotFound"] = "Игрок не найден", ["Achievement"] = "Достижение разблокировано: {0}!" }, this); } private string GetMessage(string key, string userId = null, params object[] args) { return string.Format(lang.GetMessage(key, this, userId), args); } #endregion #region Initialization void Init() { permission.RegisterPermission("statistics.use", this); permission.RegisterPermission("statistics.admin", this); LoadData(); MigrateData(); } void OnServerInitialized() { // Запускаем периодическое сохранение данных каждые 5 минут timer.Every(300f, () => { UpdateAllPlayTimes(); SaveData(); }); // Запускаем очистку старых ежедневных статистик каждый час timer.Every(3600f, () => { CleanupOldDailyStats(); }); PrintWarning($"Statistics v{Version} загружен. База данных содержит {storedData.PlayerStatistics.Count} игроков"); } #endregion #region Data Management void LoadData() { storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(config.DatabaseFile); if (storedData == null) { storedData = new StoredData(); SaveData(); } } void SaveData() { Interface.Oxide.DataFileSystem.WriteObject(config.DatabaseFile, storedData); } #endregion #region Player Events void OnPlayerConnected(BasePlayer player) { if (player == null || !player.userID.IsSteamId()) return; playerConnectTimes[player.userID] = DateTime.Now; var stats = GetPlayerStats(player.userID, player.displayName); stats.LastSeen = DateTime.Now; } void OnPlayerDisconnected(BasePlayer player, string reason) { if (player == null || !player.userID.IsSteamId()) return; UpdatePlayTime(player); playerConnectTimes.Remove(player.userID); // Закрываем UI если открыт DestroyUI(player); } private PlayerStats GetPlayerStats(ulong playerId, string playerName) { if (!storedData.PlayerStatistics.ContainsKey(playerId)) { storedData.PlayerStatistics[playerId] = new PlayerStats { PlayerName = playerName, FirstJoin = DateTime.Now, LastSeen = DateTime.Now }; } else { storedData.PlayerStatistics[playerId].PlayerName = playerName; storedData.PlayerStatistics[playerId].LastSeen = DateTime.Now; } return storedData.PlayerStatistics[playerId]; } private void UpdatePlayTime(BasePlayer player) { if (!playerConnectTimes.ContainsKey(player.userID)) return; var sessionTime = DateTime.Now - playerConnectTimes[player.userID]; var stats = GetPlayerStats(player.userID, player.displayName); stats.PlayTime = stats.PlayTime.Add(sessionTime); // Обновляем ежедневную статистику string today = DateTime.Now.ToString("yyyy-MM-dd"); if (!stats.DailyStatistics.ContainsKey(today)) stats.DailyStatistics[today] = new DailyStats(); stats.DailyStatistics[today].PlayTime = stats.DailyStatistics[today].PlayTime.Add(sessionTime); playerConnectTimes[player.userID] = DateTime.Now; } #endregion #region Combat Events void OnEntityDeath(BaseCombatEntity entity, HitInfo info) { if (!config.EnableKillStats) return; var victim = entity as BasePlayer; var attacker = info?.InitiatorPlayer; if (victim == null || !victim.userID.IsSteamId()) return; // Обрабатываем смерть var victimStats = GetPlayerStats(victim.userID, victim.displayName); victimStats.Deaths++; storedData.GlobalStatistics.TotalDeaths++; // Обновляем ежедневную статистику жертвы string today = DateTime.Now.ToString("yyyy-MM-dd"); if (!victimStats.DailyStatistics.ContainsKey(today)) victimStats.DailyStatistics[today] = new DailyStats(); victimStats.DailyStatistics[today].Deaths++; // Если есть атакующий игрок if (attacker != null && attacker.userID.IsSteamId() && attacker != victim) { var attackerStats = GetPlayerStats(attacker.userID, attacker.displayName); attackerStats.Kills++; storedData.GlobalStatistics.TotalKills++; // Обновляем ежедневную статистику атакующего if (!attackerStats.DailyStatistics.ContainsKey(today)) attackerStats.DailyStatistics[today] = new DailyStats(); attackerStats.DailyStatistics[today].Kills++; // Обрабатываем оружие string weaponName = GetWeaponName(info); if (!attackerStats.WeaponKills.ContainsKey(weaponName)) attackerStats.WeaponKills[weaponName] = 0; attackerStats.WeaponKills[weaponName]++; // Проверяем хедшот if (IsHeadshot(info)) { attackerStats.Headshots++; } // Проверяем рекорды CheckRecords(attacker, victim, info); // Проверяем достижения CheckAchievements(attacker, attackerStats); } } private string GetWeaponName(HitInfo info) { if (info?.Weapon?.GetItem()?.info?.shortname != null) return info.Weapon.GetItem().info.shortname; if (info?.WeaponPrefab?.ShortPrefabName != null) return info.WeaponPrefab.ShortPrefabName; return "Unknown"; } private bool IsHeadshot(HitInfo info) { return info != null && info.isHeadshot; } private void CheckRecords(BasePlayer attacker, BasePlayer victim, HitInfo info) { float distance = Vector3.Distance(attacker.transform.position, victim.transform.position); if (distance > storedData.GlobalStatistics.Records.LongestKill.Value) { storedData.GlobalStatistics.Records.LongestKill = new RecordEntry { PlayerName = attacker.displayName, PlayerId = attacker.userID, Value = distance, Date = DateTime.Now }; // Уведомляем сервер о новом рекорде Server.Broadcast($"🎯 Новый рекорд дальности убийства! {attacker.displayName} - {distance:F1}м"); } } #endregion #region Resource Events void OnDispenserGather(ResourceDispenser dispenser, BaseEntity entity, Item item) { if (!config.EnableResourceStats) return; var player = entity as BasePlayer; if (player == null || !player.userID.IsSteamId()) return; var stats = GetPlayerStats(player.userID, player.displayName); string resourceName = item.info.shortname; if (!stats.ResourcesGathered.ContainsKey(resourceName)) stats.ResourcesGathered[resourceName] = 0; stats.ResourcesGathered[resourceName] += item.amount; storedData.GlobalStatistics.TotalResourcesGathered += item.amount; // Обновляем ежедневную статистику string today = DateTime.Now.ToString("yyyy-MM-dd"); if (!stats.DailyStatistics.ContainsKey(today)) stats.DailyStatistics[today] = new DailyStats(); stats.DailyStatistics[today].ResourcesGathered += item.amount; // Проверяем достижения по ресурсам CheckResourceAchievements(player, stats); } void OnCollectiblePickup(Item item, BasePlayer player) { if (!config.EnableResourceStats || player == null || !player.userID.IsSteamId()) return; var stats = GetPlayerStats(player.userID, player.displayName); string resourceName = item.info.shortname; if (!stats.ResourcesGathered.ContainsKey(resourceName)) stats.ResourcesGathered[resourceName] = 0; stats.ResourcesGathered[resourceName] += item.amount; storedData.GlobalStatistics.TotalResourcesGathered += item.amount; // Обновляем ежедневную статистику string today = DateTime.Now.ToString("yyyy-MM-dd"); if (!stats.DailyStatistics.ContainsKey(today)) stats.DailyStatistics[today] = new DailyStats(); stats.DailyStatistics[today].ResourcesGathered += item.amount; } #endregion #region Building Events void OnEntityBuilt(Planner plan, GameObject go) { if (!config.EnableBuildingStats) return; var player = plan.GetOwnerPlayer(); if (player == null || !player.userID.IsSteamId()) return; var stats = GetPlayerStats(player.userID, player.displayName); stats.BlocksBuilt++; } void OnStructureDestroy(BaseCombatEntity entity, HitInfo info) { if (!config.EnableBuildingStats) return; var player = info?.InitiatorPlayer; if (player == null || !player.userID.IsSteamId()) return; var stats = GetPlayerStats(player.userID, player.displayName); stats.BlocksDestroyed++; } #endregion #region Achievements private void CheckAchievements(BasePlayer player, PlayerStats stats) { if (!config.ShowAchievementNotifications) return; // Проверяем достижения по убийствам foreach (int milestone in config.Achievements.KillMilestones) { string achievementId = $"kills_{milestone}"; if (stats.Kills >= milestone && !stats.Achievements.Contains(achievementId)) { stats.Achievements.Add(achievementId); NotifyAchievement(player, $"{milestone} убийств"); } } // Проверяем достижения по времени игры int playTimeHours = (int)stats.PlayTime.TotalHours; foreach (int milestone in config.Achievements.PlayTimeMilestones) { string achievementId = $"playtime_{milestone}"; if (playTimeHours >= milestone && !stats.Achievements.Contains(achievementId)) { stats.Achievements.Add(achievementId); NotifyAchievement(player, $"{milestone} часов игры"); } } } private void CheckResourceAchievements(BasePlayer player, PlayerStats stats) { if (!config.ShowAchievementNotifications) return; int totalResources = stats.ResourcesGathered.Values.Sum(); foreach (int milestone in config.Achievements.ResourceMilestones) { string achievementId = $"resources_{milestone}"; if (totalResources >= milestone && !stats.Achievements.Contains(achievementId)) { stats.Achievements.Add(achievementId); NotifyAchievement(player, $"{milestone} ресурсов собрано"); } } } private void NotifyAchievement(BasePlayer player, string achievement) { SendReply(player, GetMessage("Achievement", player.UserIDString, achievement)); // Эффект на экране player.SendConsoleCommand("gametip.showgametip", $"🏆 Достижение: {achievement}"); } #endregion #region Chat Commands [ChatCommand("stats")] void StatsCommand(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, "statistics.use")) { SendReply(player, GetMessage("NoPermission", player.UserIDString)); return; } BasePlayer target = player; if (args.Length > 0) { target = BasePlayer.Find(args[0]); if (target == null) { SendReply(player, GetMessage("PlayerNotFound", player.UserIDString)); return; } } ShowPlayerStats(player, target); } [ChatCommand("mystats")] void MyStatsCommand(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, "statistics.use")) { SendReply(player, GetMessage("NoPermission", player.UserIDString)); return; } ShowPlayerStats(player, player); } [ChatCommand("top")] void TopCommand(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, "statistics.use")) { SendReply(player, GetMessage("NoPermission", player.UserIDString)); return; } string category = args.Length > 0 ? args[0].ToLower() : "kills"; ShowTopPlayers(player, category); } [ChatCommand("statsui")] void StatsUICommand(BasePlayer player, string command, string[] args) { if (!permission.UserHasPermission(player.UserIDString, "statistics.use")) { SendReply(player, GetMessage("NoPermission", player.UserIDString)); return; } ShowStatsUI(player); } private void ShowPlayerStats(BasePlayer requester, BasePlayer target) { UpdatePlayTime(target); var stats = GetPlayerStats(target.userID, target.displayName); var sb = new StringBuilder(); sb.AppendLine($"<color=#FFD700>=== {GetMessage("StatsTitle", requester.UserIDString, target.displayName)} ===</color>"); sb.AppendLine($"<color=#FF6B6B>🔫 {GetMessage("Kills", requester.UserIDString, stats.Kills)}</color>"); sb.AppendLine($"<color=#85C1E9>💀 {GetMessage("Deaths", requester.UserIDString, stats.Deaths)}</color>"); sb.AppendLine($"<color=#96CEB4>📊 {GetMessage("KDRatio", requester.UserIDString, stats.KDRatio)}</color>"); sb.AppendLine($"<color=#F8C471>🎯 Хедшоты: {stats.Headshots} ({stats.HeadshotRatio}%)</color>"); sb.AppendLine($"<color=#BB8FCE>⏰ {GetMessage("PlayTime", requester.UserIDString, FormatTimeSpan(stats.PlayTime))}</color>"); sb.AppendLine($"<color=#82E0AA>🏗️ Построено блоков: {stats.BlocksBuilt}</color>"); sb.AppendLine($"<color=#F7DC6F>⛏️ Ресурсов собрано: {stats.ResourcesGathered.Values.Sum():N0}</color>"); sb.AppendLine($"<color=#AED6F1>🗓️ {GetMessage("FirstJoin", requester.UserIDString, stats.FirstJoin.ToString("dd.MM.yyyy"))}</color>"); sb.AppendLine($"<color=#D7BDE2>🏆 Достижений: {stats.Achievements.Count}</color>"); if (stats.WeaponKills.Count > 0) { sb.AppendLine($"<color=#FFB347>🔫 Любимое оружие: {GetFriendlyWeaponName(stats.FavoriteWeapon)}</color>"); } SendReply(requester, sb.ToString()); } private void ShowTopPlayers(BasePlayer player, string category) { var sb = new StringBuilder(); List<PlayerStats> topPlayers = new List<PlayerStats>(); switch (category) { case "kills": case "убийства": topPlayers = storedData.PlayerStatistics.Values.OrderByDescending(x => x.Kills).Take(10).ToList(); sb.AppendLine("<color=#FFD700>=== Топ 10 убийц ===</color>"); for (int i = 0; i < topPlayers.Count; i++) { var stats = topPlayers[i]; sb.AppendLine($"<color=#FF6B6B>{i + 1}. {stats.PlayerName} - {stats.Kills} убийств</color>"); } break; case "playtime": case "время": topPlayers = storedData.PlayerStatistics.Values.OrderByDescending(x => x.PlayTime.TotalHours).Take(10).ToList(); sb.AppendLine("<color=#FFD700>=== Топ 10 по времени игры ===</color>"); for (int i = 0; i < topPlayers.Count; i++) { var stats = topPlayers[i]; sb.AppendLine($"<color=#BB8FCE>{i + 1}. {stats.PlayerName} - {FormatTimeSpan(stats.PlayTime)}</color>"); } break; case "kd": case "кд": topPlayers = storedData.PlayerStatistics.Values.Where(x => x.Deaths > 0).OrderByDescending(x => x.KDRatio).Take(10).ToList(); sb.AppendLine("<color=#FFD700>=== Топ 10 по K/D ===</color>"); for (int i = 0; i < topPlayers.Count; i++) { var stats = topPlayers[i]; sb.AppendLine($"<color=#96CEB4>{i + 1}. {stats.PlayerName} - {stats.KDRatio}</color>"); } break; case "resources": case "ресурсы": topPlayers = storedData.PlayerStatistics.Values.OrderByDescending(x => x.ResourcesGathered.Values.Sum()).Take(10).ToList(); sb.AppendLine("<color=#FFD700>=== Топ 10 по ресурсам ===</color>"); for (int i = 0; i < topPlayers.Count; i++) { var stats = topPlayers[i]; sb.AppendLine($"<color=#F7DC6F>{i + 1}. {stats.PlayerName} - {stats.ResourcesGathered.Values.Sum():N0} ресурсов</color>"); } break; default: sb.AppendLine("<color=#FFD700>Доступные категории: kills, playtime, kd, resources</color>"); break; } SendReply(player, sb.ToString()); } private string FormatTimeSpan(TimeSpan timeSpan) { if (timeSpan.TotalDays >= 1) return $"{(int)timeSpan.TotalDays}д {timeSpan.Hours}ч {timeSpan.Minutes}м"; else if (timeSpan.TotalHours >= 1) return $"{timeSpan.Hours}ч {timeSpan.Minutes}м"; else return $"{timeSpan.Minutes}м"; } private string GetFriendlyWeaponName(string shortname) { var weaponNames = new Dictionary<string, string> { ["rifle.ak"] = "AK-47", ["rifle.lr300"] = "LR-300", ["rifle.bolt"] = "Болтовка", ["bow.hunting"] = "Охотничий лук", ["crossbow"] = "Арбалет", ["pistol.revolver"] = "Револьвер", ["shotgun.pump"] = "Помповое ружье", ["smg.thompson"] = "Томпсон" }; return weaponNames.ContainsKey(shortname) ? weaponNames[shortname] : shortname; } #endregion #region UI System private void ShowStatsUI(BasePlayer player) { DestroyUI(player); var stats = GetPlayerStats(player.userID, player.displayName); UpdatePlayTime(player); var elements = new CuiElementContainer(); // Главная панель elements.Add(new CuiPanel { Image = { Color = config.Colors.Background }, RectTransform = { AnchorMin = "0.2 0.2", AnchorMax = "0.8 0.8" }, CursorEnabled = true }, "Overlay", UIMainName); // Заголовок elements.Add(new CuiLabel { Text = { Text = $"Статистика - {player.displayName}", FontSize = 20, Align = TextAnchor.MiddleCenter, Color = config.Colors.Text }, RectTransform = { AnchorMin = "0 0.9", AnchorMax = "1 1" } }, UIMainName); // Статистика убийств elements.Add(new CuiPanel { Image = { Color = config.Colors.Panel }, RectTransform = { AnchorMin = "0.02 0.7", AnchorMax = "0.48 0.88" } }, UIMainName, "KillsPanel"); elements.Add(new CuiLabel { Text = { Text = "🔫 БОЕВАЯ СТАТИСТИКА", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 0.4 0.4 1" }, RectTransform = { AnchorMin = "0 0.8", AnchorMax = "1 1" } }, "KillsPanel"); elements.Add(new CuiLabel { Text = { Text = $"Убийства: {stats.Kills}\nСмерти: {stats.Deaths}\nK/D: {stats.KDRatio}\nХедшоты: {stats.Headshots} ({stats.HeadshotRatio}%)", FontSize = 12, Align = TextAnchor.MiddleLeft, Color = config.Colors.Text }, RectTransform = { AnchorMin = "0.05 0.1", AnchorMax = "0.95 0.8" } }, "KillsPanel"); // Статистика времени и ресурсов elements.Add(new CuiPanel { Image = { Color = config.Colors.Panel }, RectTransform = { AnchorMin = "0.52 0.7", AnchorMax = "0.98 0.88" } }, UIMainName, "GeneralPanel"); elements.Add(new CuiLabel { Text = { Text = "⏰ ОБЩАЯ СТАТИСТИКА", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "0.3 0.8 0.8 1" }, RectTransform = { AnchorMin = "0 0.8", AnchorMax = "1 1" } }, "GeneralPanel"); elements.Add(new CuiLabel { Text = { Text = $"Время игры: {FormatTimeSpan(stats.PlayTime)}\nПостроено: {stats.BlocksBuilt}\nРесурсов: {stats.ResourcesGathered.Values.Sum():N0}\nДостижений: {stats.Achievements.Count}", FontSize = 12, Align = TextAnchor.MiddleLeft, Color = config.Colors.Text }, RectTransform = { AnchorMin = "0.05 0.1", AnchorMax = "0.95 0.8" } }, "GeneralPanel"); // Топ оружия elements.Add(new CuiPanel { Image = { Color = config.Colors.Panel }, RectTransform = { AnchorMin = "0.02 0.4", AnchorMax = "0.48 0.68" } }, UIMainName, "WeaponsPanel"); elements.Add(new CuiLabel { Text = { Text = "🔫 ЛЮБИМОЕ ОРУЖИЕ", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 0.7 0.3 1" }, RectTransform = { AnchorMin = "0 0.8", AnchorMax = "1 1" } }, "WeaponsPanel"); var topWeapons = stats.WeaponKills.OrderByDescending(x => x.Value).Take(5); string weaponsText = ""; foreach (var weapon in topWeapons) { weaponsText += $"{GetFriendlyWeaponName(weapon.Key)}: {weapon.Value}\n"; } if (string.IsNullOrEmpty(weaponsText)) weaponsText = "Нет данных"; elements.Add(new CuiLabel { Text = { Text = weaponsText, FontSize = 12, Align = TextAnchor.MiddleLeft, Color = config.Colors.Text }, RectTransform = { AnchorMin = "0.05 0.1", AnchorMax = "0.95 0.8" } }, "WeaponsPanel"); // Топ ресурсов elements.Add(new CuiPanel { Image = { Color = config.Colors.Panel }, RectTransform = { AnchorMin = "0.52 0.4", AnchorMax = "0.98 0.68" } }, UIMainName, "ResourcesPanel"); elements.Add(new CuiLabel { Text = { Text = "⛏️ СОБРАННЫЕ РЕСУРСЫ", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "0.6 0.8 0.7 1" }, RectTransform = { AnchorMin = "0 0.8", AnchorMax = "1 1" } }, "ResourcesPanel"); var topResources = stats.ResourcesGathered.OrderByDescending(x => x.Value).Take(5); string resourcesText = ""; foreach (var resource in topResources) { resourcesText += $"{GetFriendlyResourceName(resource.Key)}: {resource.Value:N0}\n"; } if (string.IsNullOrEmpty(resourcesText)) resourcesText = "Нет данных"; elements.Add(new CuiLabel { Text = { Text = resourcesText, FontSize = 12, Align = TextAnchor.MiddleLeft, Color = config.Colors.Text }, RectTransform = { AnchorMin = "0.05 0.1", AnchorMax = "0.95 0.8" } }, "ResourcesPanel"); // Достижения elements.Add(new CuiPanel { Image = { Color = config.Colors.Panel }, RectTransform = { AnchorMin = "0.02 0.1", AnchorMax = "0.98 0.38" } }, UIMainName, "AchievementsPanel"); elements.Add(new CuiLabel { Text = { Text = "🏆 ПОСЛЕДНИЕ ДОСТИЖЕНИЯ", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 0.84 0 1" }, RectTransform = { AnchorMin = "0 0.85", AnchorMax = "1 1" } }, "AchievementsPanel"); string achievementsText = ""; var recentAchievements = stats.Achievements.TakeLast(8); foreach (var achievement in recentAchievements) { achievementsText += $"• {FormatAchievement(achievement)}\n"; } if (string.IsNullOrEmpty(achievementsText)) achievementsText = "Пока нет достижений"; elements.Add(new CuiLabel { Text = { Text = achievementsText, FontSize = 11, Align = TextAnchor.MiddleLeft, Color = config.Colors.Text }, RectTransform = { AnchorMin = "0.05 0.05", AnchorMax = "0.95 0.85" } }, "AchievementsPanel"); // Кнопка закрытия elements.Add(new CuiButton { Button = { Command = "statsui.close", Color = "0.8 0.2 0.2 1" }, RectTransform = { AnchorMin = "0.85 0.02", AnchorMax = "0.98 0.08" }, Text = { Text = "Закрыть", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } }, UIMainName); // Кнопка обновления elements.Add(new CuiButton { Button = { Command = "statsui.refresh", Color = "0.2 0.6 0.2 1" }, RectTransform = { AnchorMin = "0.7 0.02", AnchorMax = "0.83 0.08" }, Text = { Text = "Обновить", FontSize = 12, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" } }, UIMainName); CuiHelper.AddUi(player, elements); // Автоматическое обновление каждые 10 секунд if (uiTimers.ContainsKey(player.userID)) uiTimers[player.userID]?.Destroy(); uiTimers[player.userID] = timer.Every(10f, () => { if (player != null && player.IsConnected) ShowStatsUI(player); }); } private void DestroyUI(BasePlayer player) { CuiHelper.DestroyUi(player, UIMainName); if (uiTimers.ContainsKey(player.userID)) { uiTimers[player.userID]?.Destroy(); uiTimers.Remove(player.userID); } } private string GetFriendlyResourceName(string shortname) { var resourceNames = new Dictionary<string, string> { ["wood"] = "Дерево", ["stones"] = "Камень", ["metal.ore"] = "Металлическая руда", ["sulfur.ore"] = "Серная руда", ["cloth"] = "Ткань", ["leather"] = "Кожа", ["fat.animal"] = "Животный жир", ["bone.fragments"] = "Костные фрагменты" }; return resourceNames.ContainsKey(shortname) ? resourceNames[shortname] : shortname; } private string FormatAchievement(string achievement) { string[] parts = achievement.Split('_'); if (parts.Length >= 2) { string type = parts[0]; string value = parts[1]; switch (type) { case "kills": return $"{value} убийств"; case "playtime": return $"{value} часов игры"; case "resources": return $"{value} ресурсов собрано"; } } return achievement; } [ConsoleCommand("statsui.close")] void CloseUICommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null) DestroyUI(player); } [ConsoleCommand("statsui.refresh")] void RefreshUICommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null) ShowStatsUI(player); } #endregion #region Console Commands [ConsoleCommand("stats.reset")] void ResetStatsCommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, "statistics.admin")) { arg.ReplyWith(GetMessage("NoPermission", player?.UserIDString)); return; } if (arg.Args.Length == 0) { arg.ReplyWith("Использование: stats.reset <steamid>"); return; } if (ulong.TryParse(arg.Args[0], out ulong targetId)) { if (storedData.PlayerStatistics.ContainsKey(targetId)) { storedData.PlayerStatistics.Remove(targetId); RecalculateGlobalStats(); SaveData(); arg.ReplyWith($"Статистика игрока {targetId} сброшена"); } else { arg.ReplyWith("Игрок не найден в базе данных"); } } else { arg.ReplyWith("Неверный Steam ID"); } } [ConsoleCommand("stats.resetall")] void ResetAllStatsCommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, "statistics.admin")) { arg.ReplyWith(GetMessage("NoPermission", player?.UserIDString)); return; } storedData = new StoredData(); SaveData(); arg.ReplyWith("Вся статистика сброшена"); } [ConsoleCommand("stats.info")] void InfoCommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, "statistics.admin")) { arg.ReplyWith(GetMessage("NoPermission", player?.UserIDString)); return; } RecalculateGlobalStats(); arg.ReplyWith($"=== Информация о статистике ==="); arg.ReplyWith($"Всего игроков в базе: {storedData.PlayerStatistics.Count}"); arg.ReplyWith($"Общее количество убийств: {storedData.GlobalStatistics.TotalKills}"); arg.ReplyWith($"Общее количество смертей: {storedData.GlobalStatistics.TotalDeaths}"); arg.ReplyWith($"Общее время игры: {FormatTimeSpan(storedData.GlobalStatistics.TotalPlayTime)}"); arg.ReplyWith($"Рекорд дальности убийства: {storedData.GlobalStatistics.Records.LongestKill.Value:F1}м ({storedData.GlobalStatistics.Records.LongestKill.PlayerName})"); } [ConsoleCommand("stats.cleanup")] void CleanupCommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, "statistics.admin")) { arg.ReplyWith(GetMessage("NoPermission", player?.UserIDString)); return; } CleanupOldDailyStats(); arg.ReplyWith("Очистка старых данных завершена"); } [ConsoleCommand("stats.backup")] void BackupCommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, "statistics.admin")) { arg.ReplyWith(GetMessage("NoPermission", player?.UserIDString)); return; } try { string backupName = $"{config.DatabaseFile}_backup_{DateTime.Now:yyyyMMdd_HHmmss}"; Interface.Oxide.DataFileSystem.WriteObject(backupName, storedData); arg.ReplyWith($"Резервная копия создана: {backupName}.json"); } catch (Exception ex) { arg.ReplyWith($"Ошибка создания резервной копии: {ex.Message}"); } } [ConsoleCommand("stats.recalculate")] void RecalculateCommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, "statistics.admin")) { arg.ReplyWith(GetMessage("NoPermission", player?.UserIDString)); return; } RecalculateGlobalStats(); SaveData(); arg.ReplyWith("Глобальная статистика пересчитана"); } private void RecalculateGlobalStats() { storedData.GlobalStatistics.TotalKills = storedData.PlayerStatistics.Values.Sum(x => x.Kills); storedData.GlobalStatistics.TotalDeaths = storedData.PlayerStatistics.Values.Sum(x => x.Deaths); storedData.GlobalStatistics.TotalPlayTime = TimeSpan.FromTicks(storedData.PlayerStatistics.Values.Sum(x => x.PlayTime.Ticks)); storedData.GlobalStatistics.TotalResourcesGathered = storedData.PlayerStatistics.Values.Sum(x => x.ResourcesGathered.Values.Sum()); } #endregion #region Utility Methods private void UpdateAllPlayTimes() { foreach (var player in BasePlayer.activePlayerList) { if (player != null && player.userID.IsSteamId()) { UpdatePlayTime(player); } } } private void CleanupOldDailyStats() { var cutoffDate = DateTime.Now.AddDays(-30).ToString("yyyy-MM-dd"); int removedCount = 0; foreach (var playerStats in storedData.PlayerStatistics.Values) { var keysToRemove = playerStats.DailyStatistics.Keys .Where(date => string.Compare(date, cutoffDate) < 0) .ToList(); foreach (var key in keysToRemove) { playerStats.DailyStatistics.Remove(key); removedCount++; } } if (removedCount > 0) { PrintWarning($"Удалено {removedCount} записей старой ежедневной статистики"); SaveData(); } } private void MigrateData() { // Здесь можно добавить логику миграции данных при обновлении плагина bool needsSave = false; // Пример миграции: добавление новых полей если их нет foreach (var stats in storedData.PlayerStatistics.Values) { if (stats.DailyStatistics == null) { stats.DailyStatistics = new Dictionary<string, DailyStats>(); needsSave = true; } if (stats.Achievements == null) { stats.Achievements = new List<string>(); needsSave = true; } if (stats.WeaponKills == null) { stats.WeaponKills = new Dictionary<string, int>(); needsSave = true; } if (stats.ResourcesGathered == null) { stats.ResourcesGathered = new Dictionary<string, int>(); needsSave = true; } } if (storedData.GlobalStatistics.Records == null) { storedData.GlobalStatistics.Records = new ServerRecords(); needsSave = true; } if (needsSave) { PrintWarning("Выполнена миграция данных статистики"); SaveData(); } } #endregion #region API Methods public int GetPlayerKills(ulong playerId) { return storedData.PlayerStatistics.ContainsKey(playerId) ? storedData.PlayerStatistics[playerId].Kills : 0; } public int GetPlayerDeaths(ulong playerId) { return storedData.PlayerStatistics.ContainsKey(playerId) ? storedData.PlayerStatistics[playerId].Deaths : 0; } public double GetPlayerKDRatio(ulong playerId) { return storedData.PlayerStatistics.ContainsKey(playerId) ? storedData.PlayerStatistics[playerId].KDRatio : 0.0; } public TimeSpan GetPlayerPlayTime(ulong playerId) { return storedData.PlayerStatistics.ContainsKey(playerId) ? storedData.PlayerStatistics[playerId].PlayTime : TimeSpan.Zero; } public PlayerStats GetPlayerStats(ulong playerId) { return storedData.PlayerStatistics.ContainsKey(playerId) ? storedData.PlayerStatistics[playerId] : null; } public List<PlayerStats> GetTopKillers(int count = 10) { return storedData.PlayerStatistics.Values .OrderByDescending(x => x.Kills) .Take(count) .ToList(); } public List<PlayerStats> GetTopByPlayTime(int count = 10) { return storedData.PlayerStatistics.Values .OrderByDescending(x => x.PlayTime.TotalHours) .Take(count) .ToList(); } public void AddCustomKill(ulong playerId, string weaponName = "Unknown") { if (!storedData.PlayerStatistics.ContainsKey(playerId)) return; var stats = storedData.PlayerStatistics[playerId]; stats.Kills++; if (!stats.WeaponKills.ContainsKey(weaponName)) stats.WeaponKills[weaponName] = 0; stats.WeaponKills[weaponName]++; storedData.GlobalStatistics.TotalKills++; } public void AddCustomDeath(ulong playerId) { if (!storedData.PlayerStatistics.ContainsKey(playerId)) return; storedData.PlayerStatistics[playerId].Deaths++; storedData.GlobalStatistics.TotalDeaths++; } public GlobalStats GetGlobalStats() { return storedData.GlobalStatistics; } public ServerRecords GetServerRecords() { return storedData.GlobalStatistics.Records; } #endregion #region Web API (Optional) [ConsoleCommand("stats.webapi")] void WebAPICommand(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player != null && !permission.UserHasPermission(player.UserIDString, "statistics.admin")) { arg.ReplyWith(GetMessage("NoPermission", player?.UserIDString)); return; } if (!config.EnableWebAPI) { arg.ReplyWith("Web API отключен в конфигурации"); return; } arg.ReplyWith("Web API функционал в разработке"); } public string ExportStatsToJSON() { if (!config.EnableWebAPI) return null; try { var exportData = new { ServerInfo = new { Name = ConVar.Server.hostname, Players = BasePlayer.activePlayerList.Count, MaxPlayers = ConVar.Server.maxplayers, LastUpdate = DateTime.Now }, GlobalStats = storedData.GlobalStatistics, TopPlayers = new { Kills = storedData.PlayerStatistics.Values.OrderByDescending(x => x.Kills).Take(10), PlayTime = storedData.PlayerStatistics.Values.OrderByDescending(x => x.PlayTime.TotalHours).Take(10), KD = storedData.PlayerStatistics.Values.Where(x => x.Deaths > 0).OrderByDescending(x => x.KDRatio).Take(10) } }; return JsonConvert.SerializeObject(exportData, Formatting.Indented); } catch (Exception ex) { PrintError($"Ошибка экспорта статистики в JSON: {ex.Message}"); return null; } } public Dictionary<string, object> GetStatsForWebAPI(ulong playerId) { if (!config.EnableWebAPI) return null; if (!storedData.PlayerStatistics.ContainsKey(playerId)) return null; var stats = storedData.PlayerStatistics[playerId]; return new Dictionary<string, object> { ["playerId"] = playerId, ["playerName"] = stats.PlayerName, ["kills"] = stats.Kills, ["deaths"] = stats.Deaths, ["kdRatio"] = stats.KDRatio, ["headshots"] = stats.Headshots, ["headshotRatio"] = stats.HeadshotRatio, ["playTimeHours"] = Math.Round(stats.PlayTime.TotalHours, 2), ["blocksBuilt"] = stats.BlocksBuilt, ["favoriteWeapon"] = stats.FavoriteWeapon, ["achievements"] = stats.Achievements.Count, ["firstJoin"] = stats.FirstJoin.ToString("yyyy-MM-dd"), ["lastSeen"] = stats.LastSeen.ToString("yyyy-MM-dd"), ["totalResources"] = stats.ResourcesGathered.Values.Sum() }; } public List<Dictionary<string, object>> GetTopPlayersForWebAPI(string category = "kills", int count = 10) { if (!config.EnableWebAPI) return null; List<PlayerStats> topPlayers = new List<PlayerStats>(); switch (category.ToLower()) { case "kills": topPlayers = storedData.PlayerStatistics.Values.OrderByDescending(x => x.Kills).Take(count).ToList(); break; case "playtime": topPlayers = storedData.PlayerStatistics.Values.OrderByDescending(x => x.PlayTime.TotalHours).Take(count).ToList(); break; case "kd": topPlayers = storedData.PlayerStatistics.Values.Where(x => x.Deaths > 0).OrderByDescending(x => x.KDRatio).Take(count).ToList(); break; case "resources": topPlayers = storedData.PlayerStatistics.Values.OrderByDescending(x => x.ResourcesGathered.Values.Sum()).Take(count).ToList(); break; default: return null; } var result = new List<Dictionary<string, object>>(); for (int i = 0; i < topPlayers.Count; i++) { var stats = topPlayers[i]; var playerData = new Dictionary<string, object> { ["rank"] = i + 1, ["playerName"] = stats.PlayerName, ["kills"] = stats.Kills, ["deaths"] = stats.Deaths, ["kdRatio"] = stats.KDRatio, ["playTimeHours"] = Math.Round(stats.PlayTime.TotalHours, 2), ["totalResources"] = stats.ResourcesGathered.Values.Sum() }; result.Add(playerData); } return result; } public Dictionary<string, object> GetServerStatsForWebAPI() { if (!config.EnableWebAPI) return null; RecalculateGlobalStats(); return new Dictionary<string, object> { ["serverName"] = ConVar.Server.hostname, ["currentPlayers"] = BasePlayer.activePlayerList.Count, ["maxPlayers"] = ConVar.Server.maxplayers, ["totalRegisteredPlayers"] = storedData.PlayerStatistics.Count, ["totalKills"] = storedData.GlobalStatistics.TotalKills, ["totalDeaths"] = storedData.GlobalStatistics.TotalDeaths, ["totalPlayTimeHours"] = Math.Round(storedData.GlobalStatistics.TotalPlayTime.TotalHours, 2), ["totalResourcesGathered"] = storedData.GlobalStatistics.TotalResourcesGathered, ["longestKillDistance"] = storedData.GlobalStatistics.Records.LongestKill.Value, ["longestKillPlayer"] = storedData.GlobalStatistics.Records.LongestKill.PlayerName, ["lastUpdate"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; } #endregion #region Cleanup void Unload() { // Обновляем время игры всех активных игроков перед выгрузкой UpdateAllPlayTimes(); SaveData(); // Очищаем UI таймеры foreach (var timer in uiTimers.Values) { timer?.Destroy(); } uiTimers.Clear(); // Закрываем UI для всех игроков foreach (var player in BasePlayer.activePlayerList) { if (player != null) DestroyUI(player); } PrintWarning("Statistics плагин выгружен"); } #endregion } }