C#:
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using Network;
namespace Oxide.Plugins
{
[Info("AdminStats", "romzar.games", "2.1.0")]
[Description("Real-time server statistics HUD for administrators with drag & resize")]
public class AdminStats : RustPlugin
{
#region Constants
private const string PERMISSION_USE = "adminstats.use";
private const string UI_MAIN = "AS_Main";
private const string UI_INNER = "AS_Inner";
private const string UI_CONTENT = "AS_Content";
private const string UI_HANDLE_L = "AS_HL";
private const string UI_HANDLE_R = "AS_HR";
private const string UI_HANDLE_B = "AS_HB";
#endregion
#region Fields
private Configuration _config;
private Timer _updateTimer;
private DateTime _serverStartTime;
private readonly HashSet<ulong> _activeHuds = new HashSet<ulong>();
private readonly Queue<int> _fpsHistory = new Queue<int>();
private readonly Dictionary<ulong, PlayerData> _playerData = new Dictionary<ulong, PlayerData>();
private readonly Dictionary<ulong, float> _lastDragTime = new Dictionary<ulong, float>();
private readonly HashSet<ulong> _dirtyPlayers = new HashSet<ulong>();
private CachedMetrics _cachedMetrics = new CachedMetrics();
private bool _metricsChanged = true;
#endregion
#region Data Models
private class PanelPosition
{
[JsonProperty("ox_min")]
public float OxMin { get; set; } = 610f;
[JsonProperty("ox_max")]
public float OxMax { get; set; } = 910f;
[JsonProperty("oy_min")]
public float OyMin { get; set; } = -518f;
[JsonProperty("oy_max")]
public float OyMax { get; set; } = -302f;
public PanelPosition Copy()
{
return new PanelPosition { OxMin = OxMin, OxMax = OxMax, OyMin = OyMin, OyMax = OyMax };
}
}
private class PlayerData
{
[JsonProperty("position")]
public PanelPosition Position { get; set; } = new PanelPosition();
[JsonProperty("last_update")]
public string LastUpdate { get; set; } = DateTime.UtcNow.ToString("O");
}
#endregion
#region Data Persistence
private PlayerData EnsurePlayerData(ulong userId)
{
if (_playerData.TryGetValue(userId, out var data))
return data;
data = LoadPlayerDataSafe(userId);
if (data == null)
{
data = new PlayerData
{
Position = _config.DefaultPosition.Copy()
};
}
_playerData[userId] = data;
return data;
}
private PlayerData LoadPlayerDataSafe(ulong userId)
{
try
{
return Interface.Oxide.DataFileSystem.ReadObject<PlayerData>($"{Name}/{userId}");
}
catch { return null; }
}
private void SavePlayerData(ulong userId)
{
if (!_playerData.TryGetValue(userId, out var data)) return;
try
{
data.LastUpdate = DateTime.UtcNow.ToString("O");
Interface.Oxide.DataFileSystem.WriteObject($"{Name}/{userId}", data);
}
catch (Exception ex)
{
PrintError($"Error saving data for {userId}: {ex.Message}");
}
}
private void SaveAllDirty()
{
foreach (var userId in _dirtyPlayers)
SavePlayerData(userId);
_dirtyPlayers.Clear();
}
private PanelPosition GetPosition(ulong userId)
{
return EnsurePlayerData(userId).Position;
}
private static void ClampPanelSize(PanelPosition pos)
{
const float MIN_W = 120f;
const float MIN_H = 80f;
if (pos.OxMax - pos.OxMin < MIN_W)
{
float cx = (pos.OxMin + pos.OxMax) * 0.5f;
pos.OxMin = cx - MIN_W * 0.5f;
pos.OxMax = cx + MIN_W * 0.5f;
}
if (pos.OyMax - pos.OyMin < MIN_H)
pos.OyMin = pos.OyMax - MIN_H;
}
private bool IsPlayerDragging(ulong userId)
{
return _lastDragTime.TryGetValue(userId, out var t) && Time.realtimeSinceStartup - t < _config.DragPauseDuration;
}
#endregion
#region Cached Metrics
private class CachedMetrics
{
public int ServerFps;
public int PlayersOnline;
public int MaxPlayers;
public int EntityCount;
public long MemoryMb;
public string Uptime = "";
public string NetworkIn = "";
public string NetworkOut = "";
public bool HasChanged(CachedMetrics o)
{
return o == null || ServerFps != o.ServerFps || PlayersOnline != o.PlayersOnline ||
EntityCount != o.EntityCount || MemoryMb != o.MemoryMb ||
Uptime != o.Uptime || NetworkIn != o.NetworkIn || NetworkOut != o.NetworkOut;
}
public void CopyFrom(CachedMetrics o)
{
if (o == null) return;
ServerFps = o.ServerFps; PlayersOnline = o.PlayersOnline; MaxPlayers = o.MaxPlayers;
EntityCount = o.EntityCount; MemoryMb = o.MemoryMb; Uptime = o.Uptime;
NetworkIn = o.NetworkIn; NetworkOut = o.NetworkOut;
}
}
#endregion
#region Configuration
private class Configuration
{
[JsonProperty("Update Interval (seconds)")]
public float UpdateInterval { get; set; } = 1f;
[JsonProperty("Background Color (R G B A)")]
public string BackgroundColor { get; set; } = "0.06 0.06 0.08 0.55";
[JsonProperty("Text Color (R G B A)")]
public string TextColor { get; set; } = "0.9 0.9 0.9 1";
[JsonProperty("Header Color (R G B A)")]
public string HeaderColor { get; set; } = "0.2 0.6 0.9 1";
[JsonProperty("FPS Good Color (R G B A)")]
public string FpsGoodColor { get; set; } = "0.2 0.8 0.2 1";
[JsonProperty("FPS Warning Color (R G B A)")]
public string FpsWarningColor { get; set; } = "0.9 0.7 0.1 1";
[JsonProperty("FPS Critical Color (R G B A)")]
public string FpsCriticalColor { get; set; } = "0.9 0.2 0.2 1";
[JsonProperty("Font Size")]
public int FontSize { get; set; } = 11;
[JsonProperty("Show FPS Graph")]
public bool ShowFpsGraph { get; set; } = true;
[JsonProperty("Minimal Mode (FPS only)")]
public bool MinimalMode { get; set; } = false;
[JsonProperty("Auto-enable for admins on connect")]
public bool AutoEnableOnConnect { get; set; } = true;
[JsonProperty("Default Panel Position")]
public PanelPosition DefaultPosition { get; set; } = new PanelPosition();
[JsonProperty("Minimal Mode Position")]
public PanelPosition MinimalPosition { get; set; } = new PanelPosition
{
OxMin = 760f, OxMax = 910f, OyMin = -454f, OyMax = -302f
};
[JsonProperty("FPS Good Threshold")]
public int FpsGoodThreshold { get; set; } = 25;
[JsonProperty("FPS Warning Threshold")]
public int FpsWarningThreshold { get; set; } = 15;
[JsonProperty("FPS History Size")]
public int FpsHistorySize { get; set; } = 30;
[JsonProperty("Drag Pause Duration (seconds)")]
public float DragPauseDuration { get; set; } = 2f;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>() ?? new Configuration();
}
catch (Exception ex)
{
PrintError($"Error loading config: {ex.Message}");
_config = new Configuration();
}
SaveConfig();
}
protected override void LoadDefaultConfig() => _config = new Configuration();
protected override void SaveConfig() => Config.WriteObject(_config);
#endregion
#region Localization
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["HUD.FPS"] = "FPS",
["HUD.Players"] = "Players",
["HUD.Entities"] = "Entities",
["HUD.Memory"] = "Memory",
["HUD.NetworkIn"] = "Net In",
["HUD.NetworkOut"] = "Net Out",
["HUD.Uptime"] = "Uptime",
["Command.Enabled"] = "AdminStats HUD <color=#00ff00>enabled</color>.",
["Command.Disabled"] = "AdminStats HUD <color=#ff0000>disabled</color>.",
["Command.ModeMinimal"] = "Switched to <color=#00ff00>minimal</color> mode.",
["Command.ModeFull"] = "Switched to <color=#00ff00>full</color> mode.",
["Command.Reset"] = "Panel position <color=#00ff00>reset</color>.",
["NoPermission"] = "You do not have permission to use this command."
}, this);
lang.RegisterMessages(new Dictionary<string, string>
{
["HUD.FPS"] = "FPS",
["HUD.Players"] = "Jogadores",
["HUD.Entities"] = "Entidades",
["HUD.Memory"] = "Memoria",
["HUD.NetworkIn"] = "Rede In",
["HUD.NetworkOut"] = "Rede Out",
["HUD.Uptime"] = "Uptime",
["Command.Enabled"] = "AdminStats HUD <color=#00ff00>ativado</color>.",
["Command.Disabled"] = "AdminStats HUD <color=#ff0000>desativado</color>.",
["Command.ModeMinimal"] = "Modo <color=#00ff00>minimal</color> ativado.",
["Command.ModeFull"] = "Modo <color=#00ff00>completo</color> ativado.",
["Command.Reset"] = "Posicao <color=#00ff00>resetada</color>.",
["NoPermission"] = "Sem permissao."
}, this, "pt-BR");
lang.RegisterMessages(new Dictionary<string, string>
{
["HUD.FPS"] = "FPS",
["HUD.Players"] = "Jugadores",
["HUD.Entities"] = "Entidades",
["HUD.Memory"] = "Memoria",
["HUD.NetworkIn"] = "Red In",
["HUD.NetworkOut"] = "Red Out",
["HUD.Uptime"] = "Tiempo",
["Command.Enabled"] = "AdminStats HUD <color=#00ff00>activado</color>.",
["Command.Disabled"] = "AdminStats HUD <color=#ff0000>desactivado</color>.",
["Command.ModeMinimal"] = "Modo <color=#00ff00>minimal</color> activado.",
["Command.ModeFull"] = "Modo <color=#00ff00>completo</color> activado.",
["Command.Reset"] = "Posicion <color=#00ff00>reseteada</color>.",
["NoPermission"] = "Sin permiso."
}, this, "es-ES");
}
private string Lang(string key, string userId = null) => lang.GetMessage(key, this, userId);
#endregion
#region Oxide Hooks
private void Init()
{
permission.RegisterPermission(PERMISSION_USE, this);
_serverStartTime = DateTime.UtcNow;
}
private void OnServerInitialized()
{
float interval = _config.UpdateInterval + UnityEngine.Random.Range(-0.1f, 0.1f);
_updateTimer = timer.Every(interval, UpdateAllHuds);
if (_config.AutoEnableOnConnect)
{
foreach (var player in BasePlayer.activePlayerList)
{
if (player != null && HasPermission(player))
EnableHud(player);
}
}
}
private void Unload()
{
_updateTimer?.Destroy();
SaveAllDirty();
foreach (var player in BasePlayer.activePlayerList)
{
if (player != null && player.IsConnected)
CuiHelper.DestroyUi(player, UI_MAIN);
}
_activeHuds.Clear();
_playerData.Clear();
_lastDragTime.Clear();
_dirtyPlayers.Clear();
_fpsHistory.Clear();
}
private void OnServerSave() => SaveAllDirty();
private void OnPlayerConnected(BasePlayer player)
{
if (player == null) return;
if (_config.AutoEnableOnConnect && HasPermission(player))
{
timer.Once(2f, () =>
{
if (player != null && player.IsConnected && HasPermission(player))
EnableHud(player);
});
}
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
var userId = (ulong)player.userID;
_activeHuds.Remove(userId);
_lastDragTime.Remove(userId);
if (_dirtyPlayers.Remove(userId))
SavePlayerData(userId);
_playerData.Remove(userId);
}
#endregion
#region Drag & Resize
private void OnCuiDraggableDrag(BasePlayer player, string name, Vector3 position, CommunityEntity.DraggablePositionSendType type)
{
if (player == null) return;
var userId = (ulong)player.userID;
if (!_activeHuds.Contains(userId)) return;
var pos = GetPosition(userId);
_lastDragTime[userId] = Time.realtimeSinceStartup;
_dirtyPlayers.Add(userId);
switch (name)
{
case UI_MAIN:
pos.OxMin += position.x;
pos.OxMax += position.x;
pos.OyMin += position.y;
pos.OyMax += position.y;
break;
case UI_HANDLE_L:
pos.OxMin += position.x;
ClampPanelSize(pos);
RebuildPanel(player);
break;
case UI_HANDLE_R:
pos.OxMax += position.x;
ClampPanelSize(pos);
RebuildPanel(player);
break;
case UI_HANDLE_B:
pos.OyMin += position.y;
ClampPanelSize(pos);
RebuildPanel(player);
break;
}
}
private static CuiDraggableComponent CreateDraggable(CommunityEntity.DraggablePositionSendType rpc, bool limitToParent)
{
return new CuiDraggableComponent
{
LimitToParent = limitToParent,
ParentLimitIndex = -1,
DragAlpha = 0.75f,
PositionRPC = rpc
};
}
#endregion
#region Commands
[ChatCommand("adminstats")]
private void CmdAdminStats(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!HasPermission(player))
{
SendReply(player, Lang("NoPermission", player.UserIDString));
return;
}
if (args.Length > 0)
{
switch (args[0].ToLower())
{
case "reset":
var resetId = (ulong)player.userID;
_playerData.Remove(resetId);
_dirtyPlayers.Remove(resetId);
try { Interface.Oxide.DataFileSystem.DeleteDataFile($"{Name}/{resetId}"); } catch { }
SendReply(player, Lang("Command.Reset", player.UserIDString));
if (_activeHuds.Contains(resetId)) RebuildPanel(player);
return;
case "mode":
_config.MinimalMode = !_config.MinimalMode;
SaveConfig();
SendReply(player, Lang(_config.MinimalMode ? "Command.ModeMinimal" : "Command.ModeFull", player.UserIDString));
foreach (var uid in _activeHuds)
{
var p = BasePlayer.FindByID(uid);
if (p != null && p.IsConnected) RebuildPanel(p);
}
return;
}
}
if (_activeHuds.Contains(player.userID))
{
DisableHud(player);
SendReply(player, Lang("Command.Disabled", player.UserIDString));
}
else
{
EnableHud(player);
SendReply(player, Lang("Command.Enabled", player.UserIDString));
}
}
[ConsoleCommand("adminstats.toggle")]
private void CmdToggle(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
CmdAdminStats(player, "adminstats", new string[0]);
}
#endregion
#region Core
private bool HasPermission(BasePlayer player)
{
return player != null && permission.UserHasPermission(player.UserIDString, PERMISSION_USE);
}
private void EnableHud(BasePlayer player)
{
if (player == null || !player.IsConnected) return;
_activeHuds.Add(player.userID);
_metricsChanged = true;
RebuildPanel(player);
}
private void DisableHud(BasePlayer player)
{
if (player == null) return;
_activeHuds.Remove(player.userID);
CuiHelper.DestroyUi(player, UI_MAIN);
}
private void RebuildPanel(BasePlayer player)
{
CuiHelper.DestroyUi(player, UI_MAIN);
BuildPanel(player);
}
private void UpdateAllHuds()
{
var newMetrics = CollectMetrics();
UpdateFpsHistory(newMetrics.ServerFps);
if (!_cachedMetrics.HasChanged(newMetrics) && _fpsHistory.Count == _config.FpsHistorySize)
return;
_cachedMetrics.CopyFrom(newMetrics);
_metricsChanged = true;
foreach (var userId in _activeHuds)
{
if (IsPlayerDragging(userId)) continue;
var player = BasePlayer.FindByID(userId);
if (player != null && player.IsConnected)
RefreshContent(player);
}
_metricsChanged = false;
}
private void RefreshContent(BasePlayer player)
{
CuiHelper.DestroyUi(player, UI_CONTENT);
var pos = GetPosition(player.userID);
var container = BuildContentElements(player.UserIDString, pos);
CuiHelper.AddUi(player, container);
}
private CachedMetrics CollectMetrics()
{
return new CachedMetrics
{
ServerFps = Performance.current.frameRate,
PlayersOnline = BasePlayer.activePlayerList.Count,
MaxPlayers = ConVar.Server.maxplayers,
EntityCount = BaseNetworkable.serverEntities.Count,
MemoryMb = GC.GetTotalMemory(false) / (1024 * 1024),
Uptime = FormatUptime(DateTime.UtcNow - _serverStartTime),
NetworkIn = GetNetworkStat(true),
NetworkOut = GetNetworkStat(false)
};
}
private string GetNetworkStat(bool incoming)
{
try
{
if (Network.Net.sv == null) return "N/A";
ulong bytes = incoming
? Network.Net.sv.GetStat(null, BaseNetwork.StatTypeLong.BytesReceived_LastSecond)
: Network.Net.sv.GetStat(null, BaseNetwork.StatTypeLong.BytesSent_LastSecond);
return FormatBytes(bytes);
}
catch { return "N/A"; }
}
private void UpdateFpsHistory(int fps)
{
_fpsHistory.Enqueue(fps);
while (_fpsHistory.Count > _config.FpsHistorySize) _fpsHistory.Dequeue();
}
#endregion
#region Build Panel
private void BuildPanel(BasePlayer player)
{
var pos = GetPosition(player.userID);
var container = new CuiElementContainer();
// Main draggable panel (no cursor - works when inventory/map open)
var mainElement = new CuiElement
{
Name = UI_MAIN,
Parent = "Overlay",
Components =
{
new CuiImageComponent
{
Color = _config.BackgroundColor,
Material = "assets/content/ui/uibackgroundblur.mat"
},
new CuiRectTransformComponent
{
AnchorMin = "0.5 0.5",
AnchorMax = "0.5 0.5",
OffsetMin = $"{pos.OxMin} {pos.OyMin}",
OffsetMax = $"{pos.OxMax} {pos.OyMax}"
}
}
};
mainElement.Components.Add(CreateDraggable(CommunityEntity.DraggablePositionSendType.Relative, true));
container.Add(mainElement);
// Border
container.Add(new CuiPanel
{
Image = { Color = "0.30 0.30 0.35 0.3" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1", OffsetMin = "-1 -1", OffsetMax = "1 1" }
}, UI_MAIN, "AS_Border");
container.Add(new CuiPanel
{
Image = { Color = "0.06 0.06 0.08 0.6" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
}, "AS_Border", UI_INNER);
// Resize handles (always present, same pattern as EventCooldowns)
// Left handle
var handleLeft = new CuiElement
{
Name = UI_HANDLE_L,
Parent = UI_INNER,
Components =
{
new CuiImageComponent { Color = "0.35 0.35 0.42 0.50" },
new CuiRectTransformComponent
{
AnchorMin = "0 0.05",
AnchorMax = "0 0.95",
OffsetMin = "-12 0",
OffsetMax = "0 0"
}
}
};
handleLeft.Components.Add(CreateDraggable(CommunityEntity.DraggablePositionSendType.Relative, false));
container.Add(handleLeft);
container.Add(new CuiLabel
{
Text = { Text = "◄", FontSize = 10, Align = TextAnchor.MiddleCenter, Color = "0.85 0.85 0.90 0.80" },
RectTransform = { AnchorMin = "0 0.4", AnchorMax = "1 0.6" }
}, UI_HANDLE_L);
// Right handle
var handleRight = new CuiElement
{
Name = UI_HANDLE_R,
Parent = UI_INNER,
Components =
{
new CuiImageComponent { Color = "0.35 0.35 0.42 0.50" },
new CuiRectTransformComponent
{
AnchorMin = "1 0.05",
AnchorMax = "1 0.95",
OffsetMin = "0 0",
OffsetMax = "12 0"
}
}
};
handleRight.Components.Add(CreateDraggable(CommunityEntity.DraggablePositionSendType.Relative, false));
container.Add(handleRight);
container.Add(new CuiLabel
{
Text = { Text = "►", FontSize = 10, Align = TextAnchor.MiddleCenter, Color = "0.85 0.85 0.90 0.80" },
RectTransform = { AnchorMin = "0 0.4", AnchorMax = "1 0.6" }
}, UI_HANDLE_R);
// Bottom handle
var handleBottom = new CuiElement
{
Name = UI_HANDLE_B,
Parent = UI_INNER,
Components =
{
new CuiImageComponent { Color = "0.35 0.35 0.42 0.50" },
new CuiRectTransformComponent
{
AnchorMin = "0.05 0",
AnchorMax = "0.95 0",
OffsetMin = "0 -12",
OffsetMax = "0 0"
}
}
};
handleBottom.Components.Add(CreateDraggable(CommunityEntity.DraggablePositionSendType.Relative, false));
container.Add(handleBottom);
container.Add(new CuiLabel
{
Text = { Text = "▼", FontSize = 10, Align = TextAnchor.MiddleCenter, Color = "0.85 0.85 0.90 0.80" },
RectTransform = { AnchorMin = "0.4 0", AnchorMax = "0.6 1" }
}, UI_HANDLE_B);
CuiHelper.AddUi(player, container);
// Build content separately (for partial refresh)
var contentContainer = BuildContentElements(player.UserIDString, pos);
CuiHelper.AddUi(player, contentContainer);
}
private CuiElementContainer BuildContentElements(string userId, PanelPosition pos)
{
var container = new CuiElementContainer();
container.Add(new CuiPanel
{
Image = { Color = "0 0 0 0" },
RectTransform = { AnchorMin = "0.03 0.03", AnchorMax = "0.97 0.97" }
}, UI_INNER, UI_CONTENT);
if (_config.MinimalMode)
{
string fpsColor = GetFpsColor(_cachedMetrics.ServerFps);
container.Add(new CuiLabel
{
Text =
{
Text = $"<color=#{ColorToHex(_config.HeaderColor)}>FPS</color> <color=#{ColorToHex(fpsColor)}>{_cachedMetrics.ServerFps}</color>",
FontSize = _config.FontSize + 2,
Align = TextAnchor.MiddleCenter,
Color = _config.TextColor
},
RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
}, UI_CONTENT);
return container;
}
// Full mode stats
float yPos = 0.97f;
float rowH = 0.115f;
string fpsC = GetFpsColor(_cachedMetrics.ServerFps);
AddRow(container, userId, Lang("HUD.FPS", userId),
$"<color=#{ColorToHex(fpsC)}>{_cachedMetrics.ServerFps}</color>", ref yPos, rowH);
if (_config.ShowFpsGraph && _fpsHistory.Count > 0)
{
BuildFpsGraph(container, yPos);
yPos -= 0.07f;
}
AddRow(container, userId, Lang("HUD.Players", userId),
$"{_cachedMetrics.PlayersOnline}/{_cachedMetrics.MaxPlayers}", ref yPos, rowH);
AddRow(container, userId, Lang("HUD.Entities", userId),
FormatNumber(_cachedMetrics.EntityCount), ref yPos, rowH);
AddRow(container, userId, Lang("HUD.Memory", userId),
$"{_cachedMetrics.MemoryMb} MB", ref yPos, rowH);
AddRow(container, userId, Lang("HUD.NetworkIn", userId),
_cachedMetrics.NetworkIn, ref yPos, rowH);
AddRow(container, userId, Lang("HUD.NetworkOut", userId),
_cachedMetrics.NetworkOut, ref yPos, rowH);
AddRow(container, userId, Lang("HUD.Uptime", userId),
_cachedMetrics.Uptime, ref yPos, rowH);
return container;
}
private void AddRow(CuiElementContainer container, string userId, string label, string value, ref float yPos, float h)
{
container.Add(new CuiLabel
{
Text = { Text = label, FontSize = _config.FontSize, Align = TextAnchor.MiddleLeft, Color = "0.7 0.7 0.7 1" },
RectTransform = { AnchorMin = $"0.02 {yPos - h}", AnchorMax = $"0.48 {yPos}" }
}, UI_CONTENT);
container.Add(new CuiLabel
{
Text = { Text = value, FontSize = _config.FontSize, Align = TextAnchor.MiddleRight, Color = _config.TextColor },
RectTransform = { AnchorMin = $"0.50 {yPos - h}", AnchorMax = $"0.98 {yPos}" }
}, UI_CONTENT);
yPos -= h;
}
private void BuildFpsGraph(CuiElementContainer container, float yPos)
{
if (_fpsHistory.Count == 0) return;
float gH = 0.06f;
container.Add(new CuiPanel
{
Image = { Color = "0.05 0.05 0.05 0.8" },
RectTransform = { AnchorMin = $"0.35 {yPos - gH}", AnchorMax = $"0.98 {yPos}" }
}, UI_CONTENT, "AS_Graph");
var arr = _fpsHistory.ToArray();
float bw = 1f / _config.FpsHistorySize;
for (int i = 0; i < arr.Length; i++)
{
float nh = Mathf.Max(Mathf.Clamp01(arr[i] / 60f), 0.05f);
float xMin = i * bw + 0.01f;
float xMax = (i + 1) * bw - 0.01f;
container.Add(new CuiPanel
{
Image = { Color = GetFpsColor(arr[i]) },
RectTransform = { AnchorMin = $"{xMin} 0.05", AnchorMax = $"{xMax} {nh * 0.9 + 0.05}" }
}, "AS_Graph");
}
}
#endregion
#region Helpers
private string FormatUptime(TimeSpan t)
{
if (t.TotalDays >= 1) return $"{(int)t.TotalDays}d {t.Hours}h";
if (t.TotalHours >= 1) return $"{(int)t.TotalHours}h {t.Minutes}m";
return $"{t.Minutes}m {t.Seconds}s";
}
private string FormatBytes(ulong b)
{
if (b >= 1048576) return $"{b / 1048576.0:F1} MB/s";
if (b >= 1024) return $"{b / 1024.0:F1} KB/s";
return $"{b} B/s";
}
private string FormatNumber(int n)
{
if (n >= 1000000) return $"{n / 1000000.0:F1}M";
if (n >= 1000) return $"{n / 1000.0:F1}K";
return n.ToString();
}
private string GetFpsColor(int fps)
{
if (fps >= _config.FpsGoodThreshold) return _config.FpsGoodColor;
if (fps >= _config.FpsWarningThreshold) return _config.FpsWarningColor;
return _config.FpsCriticalColor;
}
private string ColorToHex(string rgba)
{
if (string.IsNullOrEmpty(rgba)) return "FFFFFF";
var p = rgba.Split(' ');
if (p.Length < 3) return "FFFFFF";
try
{
int r = (int)(float.Parse(p[0], CultureInfo.InvariantCulture) * 255);
int g = (int)(float.Parse(p[1], CultureInfo.InvariantCulture) * 255);
int b = (int)(float.Parse(p[2], CultureInfo.InvariantCulture) * 255);
return $"{r:X2}{g:X2}{b:X2}";
}
catch { return "FFFFFF"; }
}
#endregion
}
}