C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using Network;
using System.Globalization;
using UnityEngine.UI;
#if CARBON
using Carbon;
using Carbon.Base;
using Carbon.Components;
using Carbon.Modules;
using Carbon.Plugins;
#else
using Oxide.Ext.CarbonAliases;
#endif
namespace Oxide.Plugins {
[Info("ZenPanel", "BradCopp", "1.0.1")]
[Description("Core panel/shared features for my Zen UI plugins.")]
#if CARBON
public class ZenPanel : CarbonPlugin
#else
public class ZenPanel : RustPlugin
#endif
{
private const string TabIdMail = "mail";
private const string TabIdTickets = "tickets";
#if !CARBON
public static CUI.Handler CuiHandler = new();
#endif
#region config
private Configuration _config;
private class Configuration {
[JsonProperty("UI - Tabs In Order (omit to hide)")]
public List<string> TabOrder = new List<string> {
TabIdMail, TabIdTickets
}
;
[JsonProperty("Notification Icon Image URL (HUD bell)")]
public string HudIconUrl = "https://i.ibb.co/FkBKbkVc/bell.png";
[JsonProperty("UI - Default Theme (Dark/Light)")]
public string DefaultTheme = "Dark";
[JsonProperty("UI - Dark Theme")]
public UiPaletteConfig DarkTheme = UiPaletteConfig.CreateDark();
[JsonProperty("UI - Light Theme")]
public UiPaletteConfig LightTheme = UiPaletteConfig.CreateLight();
[JsonProperty("UI - HUD Anchor Min")]
public string HudAnchorMin = "0.5 0.00";
[JsonProperty("UI - HUD Anchor Max")]
public string HudAnchorMax = "0.5 0.00";
[JsonProperty("UI - HUD Offset Min (px)")]
public string HudOffsetMin = "-326 4";
[JsonProperty("UI - HUD Offset Max (px)")]
public string HudOffsetMax = "-294 36";
[JsonProperty("UI - HUD Unread Counter Font Size")]
public int HudUnreadFontSize = 15;
[JsonProperty("UI - HUD Unread Counter Text Color")]
public string HudUnreadTextColor = "0.98 0.99 1.00 1.00";
[JsonProperty("UI - HUD Unread Counter Shadow Color")]
public string HudUnreadTextShadowColor = "0.04 0.05 0.07 0.98";
[JsonProperty("Timestamp Offset (minutes)")]
public int TimestampOffsetMinutes = 0;
}
private class UiPaletteConfig {
[JsonProperty("Overlay")]
public string Overlay;
[JsonProperty("Window")]
public string Window;
[JsonProperty("Header")]
public string Header;
[JsonProperty("Panel")]
public string Panel;
[JsonProperty("PanelAlt")]
public string PanelAlt;
[JsonProperty("Card")]
public string Card;
[JsonProperty("CardAlt")]
public string CardAlt;
[JsonProperty("Border")]
public string Border;
[JsonProperty("BorderStrong")]
public string BorderStrong;
[JsonProperty("Text")]
public string Text;
[JsonProperty("TextMuted")]
public string TextMuted;
[JsonProperty("Accent")]
public string Accent;
[JsonProperty("AccentSoft")]
public string AccentSoft;
[JsonProperty("AccentText")]
public string AccentText;
[JsonProperty("Success")]
public string Success;
[JsonProperty("SuccessSoft")]
public string SuccessSoft;
[JsonProperty("Warning")]
public string Warning;
[JsonProperty("WarningSoft")]
public string WarningSoft;
[JsonProperty("Danger")]
public string Danger;
[JsonProperty("DangerSoft")]
public string DangerSoft;
[JsonProperty("Offline")]
public string Offline;
[JsonProperty("OfflineSoft")]
public string OfflineSoft;
[JsonProperty("Input")]
public string Input;
[JsonProperty("AvatarFill")]
public string AvatarFill;
public static UiPaletteConfig CreateDark() {
return new UiPaletteConfig {
Overlay = "0.03 0.04 0.06 0.78", Window = "0.05 0.08 0.12 0.985", Header = "0.06 0.09 0.14 1.00", Panel = "0.07 0.10 0.16 1.00", PanelAlt = "0.08 0.11 0.17 1.00", Card = "0.11 0.15 0.22 1.00", CardAlt = "0.09 0.13 0.20 1.00", Border = "0.16 0.21 0.31 1.00", BorderStrong = "0.26 0.34 0.48 1.00", Text = "0.92 0.95 0.98 1.00", TextMuted = "0.60 0.67 0.76 1.00", Accent = "0.45 0.66 1.00 1.00", AccentSoft = "0.18 0.25 0.39 1.00", AccentText = "1.00 1.00 1.00 1.00", Success = "0.29 0.77 0.48 1.00", SuccessSoft = "0.12 0.24 0.17 1.00", Warning = "0.92 0.72 0.33 1.00", WarningSoft = "0.28 0.19 0.06 1.00", Danger = "0.92 0.42 0.42 1.00", DangerSoft = "0.28 0.11 0.11 1.00", Offline = "0.54 0.61 0.70 1.00", OfflineSoft = "0.16 0.19 0.25 1.00", Input = "0.09 0.13 0.20 1.00", AvatarFill = "0.08 0.11 0.18 1.00"
}
;
}
public static UiPaletteConfig CreateLight() {
return new UiPaletteConfig {
Overlay = "0.10 0.14 0.20 0.12", Window = "0.87 0.90 0.94 0.985", Header = "0.84 0.88 0.93 1.00", Panel = "0.86 0.89 0.93 1.00", PanelAlt = "0.83 0.87 0.92 1.00", Card = "0.92 0.94 0.97 1.00", CardAlt = "0.89 0.92 0.96 1.00", Border = "0.69 0.76 0.85 1.00", BorderStrong = "0.56 0.66 0.82 1.00", Text = "0.16 0.20 0.25 1.00", TextMuted = "0.41 0.48 0.58 1.00", Accent = "0.30 0.51 0.92 1.00", AccentSoft = "0.80 0.87 0.98 1.00", AccentText = "1.00 1.00 1.00 1.00", Success = "0.24 0.62 0.36 1.00", SuccessSoft = "0.83 0.93 0.86 1.00", Warning = "0.79 0.58 0.18 1.00", WarningSoft = "0.94 0.90 0.80 1.00", Danger = "0.79 0.34 0.34 1.00", DangerSoft = "0.95 0.86 0.86 1.00", Offline = "0.55 0.62 0.70 1.00", OfflineSoft = "0.86 0.90 0.95 1.00", Input = "0.96 0.97 0.99 1.00", AvatarFill = "0.86 0.90 0.96 1.00"
}
;
}
}
private static bool NormalizeTabOrder(ref List<string> list) {
if (list == null) {
list = new List<string>();
return true;
}
bool changed = false;
var cleaned = new List<string>(list.Count);
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < list.Count; i++) {
var raw = list[i];
var value = raw == null ? string.Empty : raw.Trim().ToLowerInvariant();
if (value.Length == 0) {
changed = true;
continue;
}
if (!seen.Add(value)) {
changed = true;
continue;
}
if (!string.Equals(raw, value, StringComparison.Ordinal)) changed = true;
cleaned.Add(value);
}
if (cleaned.Count != list.Count) changed = true;
list = cleaned;
return changed;
}
protected override void LoadDefaultConfig() {
_config = new Configuration();
_config.TabOrder = new List<string> {
TabIdMail, TabIdTickets
}
;
_config.HudAnchorMin = "0.5 0.00";
_config.HudAnchorMax = "0.5 0.00";
_config.HudOffsetMin = "-326 4";
_config.HudOffsetMax = "-294 36";
_config.TimestampOffsetMinutes = 0;
SaveConfig();
}
protected override void LoadConfig() {
base.LoadConfig();
bool changed = false;
try {
_config = Config.ReadObject<Configuration>();
if (_config == null) throw new Exception("Config is null");
}
catch (Exception ex) {
PrintWarning($"Config corrupted or invalid; loading defaults. ({ex.Message})");
LoadDefaultConfig();
return;
}
if (_config.DarkTheme == null) {
_config.DarkTheme = UiPaletteConfig.CreateDark();
changed = true;
}
if (_config.LightTheme == null) {
_config.LightTheme = UiPaletteConfig.CreateLight();
changed = true;
}
if (string.IsNullOrWhiteSpace(_config.DefaultTheme)) {
_config.DefaultTheme = "Dark";
changed = true;
}
if (string.IsNullOrWhiteSpace(_config.HudUnreadTextColor)) {
_config.HudUnreadTextColor = "0.98 0.99 1.00 1.00";
changed = true;
}
if (string.IsNullOrWhiteSpace(_config.HudUnreadTextShadowColor)) {
_config.HudUnreadTextShadowColor = "0.04 0.05 0.07 0.98";
changed = true;
}
if (_config.HudUnreadTextColor == "0.09 0.11 0.14 1.00" && _config.HudUnreadTextShadowColor == "0.96 0.97 0.99 0.95") {
_config.HudUnreadTextColor = "0.98 0.99 1.00 1.00";
_config.HudUnreadTextShadowColor = "0.04 0.05 0.07 0.98";
changed = true;
}
changed |= NormalizeTabOrder(ref _config.TabOrder);
if (changed) SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(_config, true);
#endregion
#region perms and commands
private const string PermAdmin = "zenpanel.admin";
[ChatCommand("zp")]
private void CmdCommunicationCenter(BasePlayer player, string command, string[] args) {
if (player == null || !player.IsConnected) return;
OpenMainUi(player);
}
[ChatCommand("notif_iconpos")]
private void CmdHudPosEditor(BasePlayer player, string command, string[] args) {
if (player == null) return;
if (!HasPerm(player, PermAdmin)) {
ReplyL(player, LangNoPermission);
return;
}
OpenHudEditor(player);
}
#endregion
#region theme state
private enum UiThemeMode {
Dark, Light
}
private readonly Dictionary<ulong, UiThemeMode> _uiThemes = new Dictionary<ulong, UiThemeMode>();
private UiThemeMode ParseTheme(string raw) {
if (!string.IsNullOrWhiteSpace(raw) && raw.Equals("Light", StringComparison.OrdinalIgnoreCase)) return UiThemeMode.Light;
return UiThemeMode.Dark;
}
private UiThemeMode GetThemeMode(ulong userId) {
UiThemeMode mode;
if (!_uiThemes.TryGetValue(userId, out mode)) {
mode = ParseTheme(_config.DefaultTheme);
_uiThemes[userId] = mode;
}
return mode;
}
private UiPaletteConfig GetPalette(BasePlayer player) {
return GetThemeMode(player.userID) == UiThemeMode.Light ? _config.LightTheme : _config.DarkTheme;
}
private string GetThemeToggleText(BasePlayer player) {
return GetThemeMode(player.userID) == UiThemeMode.Dark ? Lang(LangUiThemeLight, player) : Lang(LangUiThemeDark, player);
}
[ConsoleCommand("pn.theme")]
private void CcmdTheme(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
_uiThemes[player.userID] = GetThemeMode(player.userID) == UiThemeMode.Dark ? UiThemeMode.Light : UiThemeMode.Dark;
OpenMainUi(player);
}
private const string LangUiTitle = "Ui.Title";
private const string LangUiThemeDark = "Ui.ThemeDark";
private const string LangUiThemeLight = "Ui.ThemeLight";
private const string LangUiNotice = "Ui.Notice";
private const string LangUiRefresh = "Ui.Refresh";
private const string LangNoPermission = "NoPermission";
private const string LangUiNoSections = "Ui.NoSections";
private const string LangUiNoTabProvider = "Ui.NoTabProvider";
private const string LangUiRenderFailed = "Ui.RenderFailed";
private const string LangUiNoRenderHook = "Ui.NoRenderHook";
#endregion
#region ui helpers
private string CreateFullscreenRoot(LUI ui, string name, string overlayColor, bool cursor = false) {
AddPanel(ui, "Overlay", name, "0 0", "1 1", overlayColor, cursor : cursor);
return name;
}
private float GetMainTabButtonWidth(string text) {
int len = string.IsNullOrWhiteSpace(text) ? 6 : text.Trim().Length;
float width = 0.05f + (len * 0.008f);
return Mathf.Clamp(width, 0.07f, 0.15f);
}
private string CreatePixelWindow(LUI ui, string parent, string name, float widthPx, float heightPx, string borderColor, string fillColor, float borderSize = 2f) {
float halfW = Mathf.Max(1f, widthPx) * 0.5f;
float halfH = Mathf.Max(1f, heightPx) * 0.5f;
AddBorderBox(ui, parent, name, "0.5 0.5", "0.5 0.5", borderColor, fillColor, borderSize, $"{-halfW:0.#} {-halfH:0.#}", $"{halfW:0.#} {halfH:0.#}");
return name + ".inner";
}
private void AddPanel(LUI ui, string parent, string name, string anchorMin, string anchorMax, string color, string offsetMin = "0 0", string offsetMax = "0 0", bool cursor = false) {
var cont = ui.CreatePanel(parent, ToLuiPosition(anchorMin, anchorMax), ToLuiOffset(offsetMin, offsetMax), color, name);
if (cursor && cont != null) cont.AddCursor();
}
private void AddBorderBox(LUI ui, string parent, string name, string anchorMin, string anchorMax, string borderColor, string fillColor, float borderSize, string offsetMin = "0 0", string offsetMax = "0 0") {
AddPanel(ui, parent, name, anchorMin, anchorMax, borderColor, offsetMin, offsetMax);
string inner = name + ".inner";
string inOffMin = $"{borderSize:0.#} {borderSize:0.#}";
string inOffMax = $"{-borderSize:0.#} {-borderSize:0.#}";
AddPanel(ui, name, inner, "0 0", "1 1", fillColor, inOffMin, inOffMax);
}
private void AddLabel(LUI ui, string parent, string text, int size, TextAnchor align, string color, string anchorMin, string anchorMax, string offsetMin = "0 0", string offsetMax = "0 0") {
ui.CreateText(parent, ToLuiPosition(anchorMin, anchorMax), ToLuiOffset(offsetMin, offsetMax), size, color, text ?? string.Empty, align);
}
private void AddTransparentButton(LUI ui, string parent, string command, string anchorMin, string anchorMax) {
if (string.IsNullOrWhiteSpace(parent) || string.IsNullOrWhiteSpace(command)) return;
ui.CreateButton(parent, ToLuiPosition(anchorMin, anchorMax), LuiOffset.None, command, LuiColors.Transparent, false);
}
private void AddBorderButton(LUI ui, string parent, string text, int fontSize, string borderColor, string fillColor, string textColor, string command, string anchorMin, string anchorMax) {
string outer = CuiHelper.GetGuid();
AddBorderBox(ui, parent, outer, anchorMin, anchorMax, borderColor, fillColor, 1f);
string inner = outer + ".inner";
AddLabel(ui, inner, text, fontSize, TextAnchor.MiddleCenter, textColor, "0 0", "1 1");
if (!string.IsNullOrEmpty(command)) AddTransparentButton(ui, inner, command, "0 0", "1 1");
}
private void AddChip(LUI ui, string parent, string text, string bgColor, string textColor, string anchorMin, string anchorMax, int fontSize) {
string chip = CuiHelper.GetGuid();
AddBorderBox(ui, parent, chip, anchorMin, anchorMax, LuiColors.Transparent, bgColor, 0f);
AddLabel(ui, chip + ".inner", text, fontSize, TextAnchor.MiddleCenter, textColor, "0 0", "1 1");
}
private string AddScrollView(LUI ui, string parent, string name, string viewAnchorMin, string viewAnchorMax, float contentHeightPx, bool showScrollbar = false, bool startAtBottom = false) {
var vBar = new LuiScrollbar {
disabled = !showScrollbar, autoHide = !showScrollbar, size = showScrollbar ? 12f : 0f, handleColor = showScrollbar ? "1 1 1 0.65" : "0 0 0 0", highlightColor = showScrollbar ? "1 1 1 0.85" : "0 0 0 0", pressedColor = showScrollbar ? "1 1 1 1" : "0 0 0 0", trackColor = "0 0 0 0"
}
;
var hBar = new LuiScrollbar {
disabled = true, autoHide = true, size = 0f, handleColor = "0 0 0 0", highlightColor = "0 0 0 0", pressedColor = "0 0 0 0", trackColor = "0 0 0 0"
}
;
var scroll = ui.CreateScrollView(parent, ToLuiPosition(viewAnchorMin, viewAnchorMax), LuiOffset.None, true, false, ScrollRect.MovementType.Clamped, 0f, true, 0.135f, 24f, vBar, hBar, name);
if (scroll != null) {
scroll.SetScrollContent(ToLuiPosition("0 1", "1 1"), new LuiOffset(0f, -Mathf.Max(1f, contentHeightPx), 0f, 0f));
scroll.SetScrollPivot(new Vector2(0.5f, 1f));
scroll.SetScrollbarPosition(0f, startAtBottom ? 0.0001f : 1f);
}
return name;
}
private void AddDbImage(LUI ui, string parent, string imageName, string anchorMin, string anchorMax, string color = LuiColors.White) {
if (string.IsNullOrWhiteSpace(parent) || string.IsNullOrWhiteSpace(imageName)) return;
if (!_imagesRegistered) return;
#if CARBON
ui.CreateRawImageFromDb(parent, ToLuiPosition(anchorMin, anchorMax), LuiOffset.None, imageName, color);
#else
if (_oxideImageUrls.TryGetValue(imageName, out var url) && !string.IsNullOrWhiteSpace(url)) {
ui.CreateUrlImage(parent, ToLuiPosition(anchorMin, anchorMax), LuiOffset.None, url, color);
}
#endif
}
#endregion
#region ui ids
private const string UiHud = "PN.HUD";
private const string UiInbox = "PN.INBOX";
private const string UiPopup = "PN.POPUP";
private const string UiHudEdit = "PN.HUDEDIT";
#endregion
#region hud editor state
private class HudEditState {
public float StepSmall = 0.002f;
public float StepBig = 0.01f;
public float SizeSmall = 2f;
public float SizeBig = 10f;
}
private readonly Dictionary<ulong, HudEditState> _hudEdit = new Dictionary<ulong, HudEditState>();
private HudEditState GetHudEditState(ulong id) {
if (!_hudEdit.TryGetValue(id, out var s)) _hudEdit[id] = s = new HudEditState();
return s;
}
#endregion
#region permissions
private bool _permsRegistered;
private void RegisterPerms() {
if (_permsRegistered) return;
permission.RegisterPermission(PermAdmin, this);
_permsRegistered = true;
Puts($"Registered permissions: {PermAdmin}");
}
#endregion
#region server hooks
private void Init() {
RegisterPerms();
}
private void OnServerInitialized() {
timer.Once(0f, () => {
foreach (var player in BasePlayer.activePlayerList) ShowHudIcon(player);
}
);
timer.Once(1f, TryInitImages);
}
private void Unload() {
foreach (var player in BasePlayer.activePlayerList) DestroyAllUi(player);
_lastHudUnread.Clear();
_pendingApiImageSets.Clear();
_oxideImageUrls.Clear();
}
private void OnPluginUnloaded(Plugin plugin) {
if (plugin == null) return;
var pluginPrefix = SanitizeApiImageKeyPart(plugin.Name, "PLUGIN") + ":";
var pendingRemove = new List<string>();
foreach (var kv in _pendingApiImageSets) {
if (kv.Key.StartsWith(pluginPrefix, StringComparison.OrdinalIgnoreCase)) pendingRemove.Add(kv.Key);
}
for (int i = 0; i < pendingRemove.Count; i++) _pendingApiImageSets.Remove(pendingRemove[i]);
var imageKeyPrefix = SanitizeApiImageKeyPart(plugin.Name, "PLUGIN") + "_";
var oxideImageRemove = new List<string>();
foreach (var kv in _oxideImageUrls) {
if (kv.Key.StartsWith(imageKeyPrefix, StringComparison.OrdinalIgnoreCase)) oxideImageRemove.Add(kv.Key);
}
for (int i = 0; i < oxideImageRemove.Count; i++) _oxideImageUrls.Remove(oxideImageRemove[i]);
var removeIds = new List<string>();
foreach (var kv in _tabProviders) {
var provider = kv.Value;
if (provider == null) continue;
if (provider.OwnerPlugin == plugin) removeIds.Add(kv.Key);
}
for (int i = 0; i < removeIds.Count; i++) _tabProviders.Remove(removeIds[i]);
}
private void OnPlayerConnected(BasePlayer player) {
if (player == null) return;
ShowHudIcon(player);
}
private void OnPlayerDisconnected(BasePlayer player, string reason) {
if (player == null) return;
DestroyAllUi(player);
_mainUiState.Remove(player.userID);
_hudEdit.Remove(player.userID);
_uiThemes.Remove(player.userID);
_lastHudUnread.Remove(player.userID);
}
private void OnPlayerDeath(BasePlayer player, HitInfo info) {
if (player == null) return;
DestroyAllUi(player);
}
private void OnPlayerSleepEnded(BasePlayer player) {
if (player == null) return;
NextTick(() => {
if (player == null || !player.IsConnected) return;
RefreshHudIcon(player, true);
if (GetUnreadCount(player) > 0) PlayFx(player, NotificationFx);
}
);
}
#endregion
#region image db
private bool _imagesRegistered;
private const string KeyHudIcon = "PN_HUDICON";
private class ApiImageSetRegistration {
public string Id;
public string KeyPrefix;
public List<string> Urls = new List<string>();
public List<string> Keys = new List<string>();
}
private readonly Dictionary<string, ApiImageSetRegistration> _pendingApiImageSets = new Dictionary<string, ApiImageSetRegistration>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _oxideImageUrls = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private static string SanitizeApiImageKeyPart(string value, string fallback) {
value = SafeUiText(value ?? string.Empty, 64).Trim();
if (string.IsNullOrWhiteSpace(value)) value = fallback;
var sb = new System.Text.StringBuilder(value.Length);
bool lastWasUnderscore = false;
for (int i = 0; i < value.Length; i++) {
char c = value[i];
if (char.IsLetterOrDigit(c)) {
sb.Append(char.ToUpperInvariant(c));
lastWasUnderscore = false;
continue;
}
if (!lastWasUnderscore) {
sb.Append('_');
lastWasUnderscore = true;
}
}
var result = sb.ToString().Trim('_');
return string.IsNullOrWhiteSpace(result) ? fallback : result;
}
private static List<string> BuildApiImageKeys(string keyPrefix, int count) {
var keys = new List<string>(count);
for (int i = 0; i < count; i++) keys.Add($"{keyPrefix}_{i}");
return keys;
}
private bool TryQueueApiImageSet(ApiImageSetRegistration set) {
if (set == null || set.Urls == null || set.Keys == null) return false;
if (set.Urls.Count == 0 || set.Urls.Count != set.Keys.Count) return false;
#if CARBON
var imageDb = GetCarbonImageDb();
if (imageDb == null) return false;
for (int i = 0; i < set.Urls.Count; i++) {
var url = set.Urls[i];
var key = set.Keys[i];
if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(key)) continue;
imageDb.Queue(key, url);
}
return true;
#else
for (int i = 0; i < set.Urls.Count; i++) {
var url = set.Urls[i];
var key = set.Keys[i];
if (string.IsNullOrWhiteSpace(url) || string.IsNullOrWhiteSpace(key)) continue;
_oxideImageUrls[key] = url.Trim();
}
return true;
#endif
}
private void FlushPendingApiImageSets() {
if (_pendingApiImageSets.Count == 0) return;
var removeIds = new List<string>();
bool queuedAny = false;
foreach (var kv in _pendingApiImageSets) {
var reg = kv.Value;
if (reg == null) {
removeIds.Add(kv.Key);
continue;
}
if (TryQueueApiImageSet(reg)) {
removeIds.Add(kv.Key);
queuedAny = true;
}
}
for (int i = 0; i < removeIds.Count; i++) _pendingApiImageSets.Remove(removeIds[i]);
if (queuedAny) {
timer.Once(1f, RefreshAllImageConsumers);
timer.Once(3f, RefreshAllImageConsumers);
timer.Once(6f, RefreshAllImageConsumers);
}
}
private ImageDatabaseModule GetCarbonImageDb() {
#if CARBON
return Carbon.Base.BaseModule.GetModule<ImageDatabaseModule>();
#else
return null;
#endif
}
private void TryInitImages() {
if (_imagesRegistered) return;
RegisterImages();
}
private void RegisterImages() {
#if CARBON
var imageDb = GetCarbonImageDb();
if (imageDb == null) {
PrintWarning("Carbon ImageDatabaseModule not available yet. Retrying...");
timer.Once(1f, TryInitImages);
return;
}
if (!string.IsNullOrWhiteSpace(_config.HudIconUrl)) imageDb.Queue(KeyHudIcon, _config.HudIconUrl.Trim());
#else
if (!string.IsNullOrWhiteSpace(_config.HudIconUrl)) _oxideImageUrls[KeyHudIcon] = _config.HudIconUrl.Trim();
#endif
_imagesRegistered = true;
FlushPendingApiImageSets();
timer.Once(1f, RefreshAllImageConsumers);
timer.Once(3f, RefreshAllImageConsumers);
timer.Once(6f, RefreshAllImageConsumers);
}
private void RefreshAllImageConsumers() {
foreach (var player in BasePlayer.activePlayerList) {
if (player == null || !player.IsConnected) continue;
RefreshHudIcon(player, true);
}
}
#endregion
#region lui layout
private LUI CreateLui() {
var cui = new CUI(CuiHandler);
return cui.v2 ?? new LUI(cui);
}
private static Vector2 ParseVector2(string value, Vector2 fallback) {
if (string.IsNullOrWhiteSpace(value)) return fallback;
var parts = value.Trim().Split(new[] {
' '
}
, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) return fallback;
float x;
float y;
if (!float.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out x)) x = fallback.x;
if (!float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out y)) y = fallback.y;
return new Vector2(x, y);
}
private static LuiPosition ToLuiPosition(string anchorMin, string anchorMax) {
var min = ParseVector2(anchorMin, Vector2.zero);
var max = ParseVector2(anchorMax, Vector2.one);
return new LuiPosition(min.x, min.y, max.x, max.y);
}
private static LuiOffset ToLuiOffset(string offsetMin, string offsetMax) {
var min = ParseVector2(offsetMin, Vector2.zero);
var max = ParseVector2(offsetMax, Vector2.zero);
return new LuiOffset(min.x, min.y, max.x, max.y);
}
private void SendLui(BasePlayer player, LUI ui) {
if (player == null) return;
var bytes = ui.GetUiBytes();
ui.SendUiBytes(player, bytes);
}
#endregion
#region tabs
private class MainUiState {
public string SelectedTabId = string.Empty;
}
private class TabProvider {
public string Id;
public string TitleLangKey;
public string StaticTitle;
public Func<BasePlayer, bool> IsVisible;
public Func<ulong, int> GetUnreadCount;
public Action<LUI, string, BasePlayer, UiPaletteConfig> RenderContent;
public Plugin OwnerPlugin;
public string VisibleHook;
public string UnreadHook;
public string RenderHook;
}
private readonly Dictionary<ulong, MainUiState> _mainUiState = new Dictionary<ulong, MainUiState>();
private readonly Dictionary<string, TabProvider> _tabProviders = new Dictionary<string, TabProvider>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<ulong, int> _lastHudUnread = new Dictionary<ulong, int>();
private MainUiState GetMainUiState(ulong userId) {
if (!_mainUiState.TryGetValue(userId, out var st)) {
st = new MainUiState();
_mainUiState[userId] = st;
}
return st;
}
private static int ReadHookInt(object value, int fallback = 0) {
if (value == null) return fallback;
if (value is int i) return i;
if (value is long l) {
if (l > int.MaxValue) return int.MaxValue;
if (l < int.MinValue) return int.MinValue;
return (int)l;
}
if (value is float f) return Mathf.RoundToInt(f);
if (value is double d) return (int)Math.Round(d);
try {
return Convert.ToInt32(value, CultureInfo.InvariantCulture);
}
catch {
return fallback;
}
}
private Dictionary<string, string> BuildPaletteMap(UiPaletteConfig p) {
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) {
["Overlay"]
= p.Overlay, ["Window"] = p.Window, ["Header"] = p.Header, ["Panel"] = p.Panel, ["PanelAlt"] = p.PanelAlt, ["Card"] = p.Card, ["CardAlt"] = p.CardAlt, ["Border"] = p.Border, ["BorderStrong"] = p.BorderStrong, ["Text"] = p.Text, ["TextMuted"] = p.TextMuted, ["Accent"] = p.Accent, ["AccentSoft"] = p.AccentSoft, ["AccentText"] = p.AccentText, ["Success"] = p.Success, ["SuccessSoft"] = p.SuccessSoft, ["Warning"] = p.Warning, ["WarningSoft"] = p.WarningSoft, ["Danger"] = p.Danger, ["DangerSoft"] = p.DangerSoft, ["Offline"] = p.Offline, ["OfflineSoft"] = p.OfflineSoft, ["Input"] = p.Input, ["AvatarFill"] = p.AvatarFill
}
;
}
private string GetProviderTitle(BasePlayer player, TabProvider provider) {
if (provider == null) return string.Empty;
if (!string.IsNullOrWhiteSpace(provider.TitleLangKey)) return Lang(provider.TitleLangKey, player);
if (!string.IsNullOrWhiteSpace(provider.StaticTitle)) return provider.StaticTitle;
return provider.Id ?? string.Empty;
}
private bool IsProviderVisible(BasePlayer player, TabProvider provider) {
if (player == null || provider == null) return false;
if (provider.IsVisible != null) return provider.IsVisible(player);
if (provider.OwnerPlugin != null && !string.IsNullOrWhiteSpace(provider.VisibleHook)) {
try {
var result = provider.OwnerPlugin.Call(provider.VisibleHook, player, provider.Id);
if (result == null) return true;
if (result is bool visible) return visible;
return false;
}
catch (Exception ex) {
PrintWarning($"Tab '{provider.Id}' visible hook failed: {ex.Message}");
return false;
}
}
return true;
}
private int GetUnreadCount(BasePlayer player) {
return GetUnreadCount(player, player?.userID ?? 0UL);
}
private int GetUnreadCount(ulong userId) {
var player = BasePlayer.FindByID(userId) ?? BasePlayer.FindSleeping(userId);
return GetUnreadCount(player, userId);
}
private int GetUnreadCount(BasePlayer player, ulong userId) {
int unread = 0;
foreach (var provider in _tabProviders.Values) {
if (provider == null) continue;
if (player != null && !IsProviderVisible(player, provider)) continue;
unread += GetProviderUnreadCount(userId, provider);
}
return unread;
}
private int GetProviderUnreadCount(ulong userId, TabProvider provider) {
if (provider == null) return 0;
if (provider.GetUnreadCount != null) return Mathf.Max(0, provider.GetUnreadCount(userId));
if (provider.OwnerPlugin != null && !string.IsNullOrWhiteSpace(provider.UnreadHook)) {
try {
var result = provider.OwnerPlugin.Call(provider.UnreadHook, userId, provider.Id);
return Mathf.Max(0, ReadHookInt(result));
}
catch (Exception ex) {
PrintWarning($"Tab '{provider.Id}' unread hook failed: {ex.Message}");
return 0;
}
}
return 0;
}
private void RenderProviderContent(LUI ui, string root, BasePlayer player, UiPaletteConfig p, TabProvider provider) {
if (provider == null) {
AddLabel(ui, root, Lang(LangUiNoTabProvider, player), 16, TextAnchor.MiddleCenter, p.TextMuted, "0 0", "1 1");
return;
}
if (provider.RenderContent != null) {
provider.RenderContent(ui, root, player, p);
return;
}
if (provider.OwnerPlugin != null && !string.IsNullOrWhiteSpace(provider.RenderHook)) {
try {
provider.OwnerPlugin.Call(provider.RenderHook, ui, root, player, BuildPaletteMap(p), provider.Id);
return;
}
catch (Exception ex) {
PrintWarning($"Tab '{provider.Id}' render hook failed: {ex}");
AddLabel(ui, root, Lang(LangUiRenderFailed, player, provider.Id), 16, TextAnchor.MiddleCenter, p.TextMuted, "0 0", "1 1");
return;
}
}
AddLabel(ui, root, Lang(LangUiNoRenderHook, player, provider.Id), 16, TextAnchor.MiddleCenter, p.TextMuted, "0 0", "1 1");
}
private List<TabProvider> GetVisibleTabProviders(BasePlayer player) {
var result = new List<TabProvider>();
var added = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var order = _config?.TabOrder;
if (order != null && order.Count > 0) {
for (int i = 0; i < order.Count; i++) {
var id = order[i];
if (string.IsNullOrWhiteSpace(id)) continue;
TabProvider provider;
if (!_tabProviders.TryGetValue(id, out provider) || provider == null) continue;
if (!IsProviderVisible(player, provider)) continue;
if (!added.Add(provider.Id)) continue;
result.Add(provider);
}
return result;
}
foreach (var kv in _tabProviders) {
var provider = kv.Value;
if (provider == null) continue;
if (!IsProviderVisible(player, provider)) continue;
if (!added.Add(provider.Id)) continue;
result.Add(provider);
}
return result;
}
private string GetTabButtonText(BasePlayer player, TabProvider provider) {
if (provider == null) return string.Empty;
return GetProviderTitle(player, provider);
}
private string FormatTabChipText(int unread) {
if (unread <= 0) return string.Empty;
return unread > 99 ? "99+" : unread.ToString(CultureInfo.InvariantCulture);
}
private string FormatHudCountText(int unread) {
if (unread <= 0) return string.Empty;
return Mathf.Min(unread, 99).ToString(CultureInfo.InvariantCulture);
}
private void AddMainTabButton(LUI ui, string parent, string text, int unread, bool active, string command, UiPaletteConfig p, string anchorMin, string anchorMax) {
string outer = CuiHelper.GetGuid();
AddBorderBox(ui, parent, outer, anchorMin, anchorMax, active ? p.BorderStrong : p.Border, active ? p.AccentSoft : p.CardAlt, 1f);
string inner = outer + ".inner";
AddLabel(ui, inner, text ?? string.Empty, 12, TextAnchor.MiddleCenter, active ? p.Accent : p.TextMuted, "0.04 0", unread > 0 ? "0.80 1" : "0.96 1");
if (unread > 0) {
AddChip(ui, inner, FormatTabChipText(unread), p.Danger, p.AccentText, "0.79 0.20", "0.97 0.80", 9);
}
AddTransparentButton(ui, inner, command, "0 0", "1 1");
}
private void RenderMainTabBar(LUI ui, string parent, BasePlayer player, UiPaletteConfig p, string activeTabId, List<TabProvider> tabs) {
if (tabs == null || tabs.Count == 0) return;
const float startX = 0.01f;
const float endX = 0.78f;
const float gap = 0.006f;
var visibleTabs = new List<TabProvider>();
var unreadCounts = new List<int>();
float uniformWidth = 0f;
for (int i = 0; i < tabs.Count; i++) {
var tab = tabs[i];
if (tab == null) continue;
visibleTabs.Add(tab);
int unread = GetProviderUnreadCount(player.userID, tab);
unreadCounts.Add(unread);
float needed = GetMainTabButtonWidth(GetTabButtonText(player, tab));
if (needed > uniformWidth) uniformWidth = needed;
}
if (visibleTabs.Count == 0) return;
float available = endX - startX;
float totalGap = gap * (visibleTabs.Count - 1);
float maxAllowedWidth = (available - totalGap) / visibleTabs.Count;
uniformWidth = Mathf.Min(uniformWidth, maxAllowedWidth);
uniformWidth = Mathf.Clamp(uniformWidth, 0.07f, 0.15f);
float x = startX;
for (int i = 0; i < visibleTabs.Count; i++) {
var tab = visibleTabs[i];
int unread = unreadCounts[i];
AddMainTabButton(ui, parent, GetTabButtonText(player, tab), unread, string.Equals(tab.Id, activeTabId, StringComparison.OrdinalIgnoreCase), $"pn.opentab {tab.Id}", p, $"{x:0.####} 0.16", $"{(x + uniformWidth):0.####} 0.84");
x += uniformWidth + gap;
}
}
private void OpenMainUi(BasePlayer player, string preferredTabId = null) {
if (player == null || !player.IsConnected) return;
CuiHelper.DestroyUi(player, UiInbox);
CuiHelper.DestroyUi(player, UiPopup);
var p = GetPalette(player);
var mainState = GetMainUiState(player.userID);
if (!string.IsNullOrWhiteSpace(preferredTabId)) mainState.SelectedTabId = preferredTabId.Trim().ToLowerInvariant();
var visibleTabs = GetVisibleTabProviders(player);
TabProvider active = null;
if (visibleTabs.Count > 0) {
if (string.IsNullOrWhiteSpace(mainState.SelectedTabId) || visibleTabs.All(x => !string.Equals(x.Id, mainState.SelectedTabId, StringComparison.OrdinalIgnoreCase))) {
mainState.SelectedTabId = visibleTabs[0].Id;
}
active = visibleTabs.FirstOrDefault(x => string.Equals(x.Id, mainState.SelectedTabId, StringComparison.OrdinalIgnoreCase));
if (active == null) {
active = visibleTabs[0];
mainState.SelectedTabId = active.Id;
}
}
else {
mainState.SelectedTabId = string.Empty;
}
using var ui = CreateLui();
CreateFullscreenRoot(ui, UiInbox, p.Overlay, true);
string root = CreatePixelWindow(ui, UiInbox, UiInbox + ".main", 1280f, 720f, p.BorderStrong, p.Window, 2f);
AddBorderBox(ui, root, root + ".header", "0 0.93", "1 1", p.Border, p.Header, 1f);
AddLabel(ui, root + ".header.inner", Lang(LangUiTitle, player), 22, TextAnchor.MiddleLeft, p.Text, "0.02 0", "0.50 1");
AddBorderButton(ui, root + ".header.inner", GetThemeToggleText(player), 11, p.Border, p.Card, p.Text, "pn.theme main", "0.84 0.22", "0.90 0.78");
AddBorderButton(ui, root + ".header.inner", Lang(LangUiRefresh, player), 13, p.Border, p.Card, p.Text, "pn.refreshinbox", "0.905 0.22", "0.945 0.78");
AddBorderButton(ui, root + ".header.inner", "✕", 14, p.Border, p.Header, p.TextMuted, "pn.closeinbox", "0.95 0.22", "0.985 0.78");
AddBorderBox(ui, root, root + ".nav", "0 0.87", "1 0.93", p.Border, p.Window, 1f);
if (visibleTabs.Count > 0) {
RenderMainTabBar(ui, root + ".nav.inner", player, p, active != null ? active.Id : string.Empty, visibleTabs);
}
else {
AddLabel(ui, root + ".nav.inner", Lang(LangUiNoSections, player), 12, TextAnchor.MiddleLeft, p.TextMuted, "0.02 0", "0.98 1");
}
AddBorderBox(ui, root, root + ".content", "0 0", "1 0.87", p.Border, p.Window, 1f);
string content = root + ".content.inner";
if (active != null) {
RenderProviderContent(ui, content, player, p, active);
}
else {
AddLabel(ui, content, Lang(LangUiNoSections, player), 18, TextAnchor.MiddleCenter, p.TextMuted, "0 0", "1 1");
}
SendLui(player, ui);
RefreshHudIcon(player, true);
}
[ConsoleCommand("pn.openinbox")]
private void CcmdOpenInbox(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
OpenMainUi(player);
}
[ConsoleCommand("pn.opentab")]
private void CcmdOpenTab(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
var tabId = arg.GetString(0, string.Empty);
OpenMainUi(player, tabId);
}
[ConsoleCommand("pn.closeinbox")]
private void CcmdCloseInbox(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
CuiHelper.DestroyUi(player, UiInbox);
CuiHelper.DestroyUi(player, UiPopup);
}
[ConsoleCommand("pn.refreshinbox")]
private void CcmdRefreshInbox(ConsoleSystem.Arg arg) {
var player = arg.Player();
if (player == null) return;
OpenMainUi(player);
}
#endregion
#region addon api
private object API_RegisterTab(Plugin plugin, string id, string title, string renderHook, string unreadHook = null, string visibleHook = null) {
if (plugin == null) return false;
id = (id ?? string.Empty).Trim().ToLowerInvariant();
title = SafeUiText(title ?? string.Empty, 32);
renderHook = (renderHook ?? string.Empty).Trim();
unreadHook = string.IsNullOrWhiteSpace(unreadHook) ? null : unreadHook.Trim();
visibleHook = string.IsNullOrWhiteSpace(visibleHook) ? null : visibleHook.Trim();
if (string.IsNullOrWhiteSpace(id)) return false;
if (string.IsNullOrWhiteSpace(renderHook)) return false;
_tabProviders[id] = new TabProvider {
Id = id, StaticTitle = string.IsNullOrWhiteSpace(title) ? id : title, OwnerPlugin = plugin, VisibleHook = visibleHook, UnreadHook = unreadHook, RenderHook = renderHook
}
;
return true;
}
private object API_UnregisterTab(Plugin plugin, string id) {
if (plugin == null) return false;
id = (id ?? string.Empty).Trim().ToLowerInvariant();
if (string.IsNullOrWhiteSpace(id)) return false;
if (!_tabProviders.TryGetValue(id, out var provider) || provider == null) return false;
if (provider.OwnerPlugin != plugin) return false;
_tabProviders.Remove(id);
return true;
}
private object API_OpenTab(BasePlayer player, string tabId) {
if (player == null || !player.IsConnected) return false;
OpenMainUi(player, tabId);
return true;
}
private object API_RefreshUi(BasePlayer player, string preferredTabId = null) {
if (player == null || !player.IsConnected) return false;
OpenMainUi(player, preferredTabId);
return true;
}
private object API_ShowPopup(BasePlayer player, string message) {
if (player == null || !player.IsConnected) return false;
ShowPopup(player, message ?? string.Empty);
return true;
}
private object API_GetPalette(BasePlayer player) {
if (player == null) return null;
return BuildPaletteMap(GetPalette(player));
}
private object API_RefreshHud(BasePlayer player, bool force = true) {
if (player == null || !player.IsConnected) return false;
RefreshHudIcon(player, force);
return true;
}
private object API_ToggleTheme(BasePlayer player) {
if (player == null) return false;
_uiThemes[player.userID] = GetThemeMode(player.userID) == UiThemeMode.Dark ? UiThemeMode.Light : UiThemeMode.Dark;
return true;
}
// ... [Code preview truncated due to length. Full source in attached file] ...