using System; using System.Collections.Generic; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Plugins; using Oxide.Game.Rust.Cui; using UnityEngine; /* * MajestatTopInfo 1.0.0 * ===================== * Plugin informacyjny — /help (alias /info /rules /regulamin) * * TRYBY PRACY (auto-wykrywanie): * * STANDALONE — Core nieobecny lub RequireCore=false * Własny panel CUI, działa w pełni samodzielnie. * * CORE ONLY — Core >= 3.2.0, Web nieobecny lub Web < 1.2.0 * Panel wbudowany w CUI Core, brak zakładki w Web. * Jeśli Web jest w złej wersji i cfg WebIntegration=true → ERROR + brak ładowania. * Ustaw WebIntegration=false żeby ładować mimo złej wersji Web. * * FULL — Core >= 3.2.0, Web >= 1.2.0 * Panel Core + zakładka GAME INFO w dashboardzie Web. * * WYMAGANIA: * • Bez Core: brak (standalone) * • Z Core: MajestatTopCore 3.2.0+ * • Z Core + Web: MajestatTopCore 3.2.0+ i MajestatTopWeb 1.2.0+ * Jeśli Web jest załadowany ale < 1.2.0 — błąd (chyba że WebIntegration=false w cfg) * * "NIE POKAZUJ PRZY STARCIE": * Checkbox na dole panelu. Zaznaczenie zapisuje SteamID gracza. * Auto-otwarcie na połączeniu pomija zaznaczonych graczy. * /help otwiera panel zawsze, niezależnie od ustawienia. * Dane: oxide/data/MajestatTopInfo_dismissed.json */ namespace Oxide.Plugins { [Info("MajestatTopInfo", "Wo0t", "1.0.0")] [Description("Server info panel with tabs, BBCode and images. Integrates with MajestatTopCore.")] class MajestatTopInfo : RustPlugin { [PluginReference] Plugin MajestatTopCore; [PluginReference] Plugin MajestatTopWeb; private const string PluginVersion = "1.0.0"; private static readonly string PluginUpdateUrl = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String("aHR0cDovL2Rvd25sb2FkLmtvbGRyaXguY29tL3J1c3QvcGx1Z2lucy9NYWplc3RhdFRvcEluZm8vdmVyc2lvbi5qc29u")); private const string PermUse = "majestattopinfo.use"; private const string PermAdmin = "majestattopinfo.admin"; // Panel names private const string PANEL_BG = "MajestatInfoBG"; // standalone tło private const string PANEL_MAIN = "MajestatInfoMain"; // standalone treść private const string CORE_BG = "MajestatTopCoreBG"; // Core tło (do embeddowania) private const string INFO_EMBED = "MajestatInfoEmbed"; // INFO_BOTTOM removed — inline bottom bar used instead private HashSet _activePanels = new HashSet(); private HashSet _dismissed = new HashSet(); private readonly List _registeredCmds = new List(); private bool _registered = false; private int _logLevel = 1; private void MLog(int level, string msg) { int effective = MajestatTopCore != null ? (int)(MajestatTopCore.Call("API_GetLogLevel") ?? 1) : _logLevel; if (level < effective) return; // Podczas AutoReload wycisz komunikaty systemowe (1) — Core drukuje summary if (level == 1 && MajestatTopCore != null) { var ar = MajestatTopCore.Call("API_IsAutoReloading"); if (ar is bool b && b) return; } if (level >= 2) PrintWarning(msg); else Puts(msg); } // Broadcast private Timer _broadcastTimer; private int _broadcastIndex = 0; // Tryb pracy ustalany przy starcie private enum WorkMode { Standalone, CoreOnly, Full } private WorkMode _mode = WorkMode.Standalone; // ═══════════════════════════════════════════════════════════════ // KONFIGURACJA // ═══════════════════════════════════════════════════════════════ private class InfoTab { [JsonProperty("id")] public string Id = "tab1"; [JsonProperty("label")] public string Label = "Zakładka"; [JsonProperty("icon")] public string Icon = "☰"; [JsonProperty("color")] public string Color = "0.2 0.6 0.9"; [JsonProperty("lines", ObjectCreationHandling = ObjectCreationHandling.Replace)] public List Lines = new List(); } private class ConfigData { [JsonProperty("Plugin title")] public string Title = "INFORMACJE O SERWERZE"; [JsonProperty("Title color (RGBA 0-1)")] public string TitleColor = "0.3 0.85 1 1"; [JsonProperty("Tab bar width (fraction)")] public float TabBarWidth = 0.21f; [JsonProperty("Default tab id")] public string DefaultTab = "rules"; [JsonProperty("Commands (chat)", ObjectCreationHandling = ObjectCreationHandling.Replace)] public List Commands = new List { "help", "info", "rules", "regulamin" }; [JsonProperty("Show on first connect (auto-open)")] public bool ShowOnConnect = true; [JsonProperty("Show on every connect (ignore dismissed flag)")] public bool ShowEveryConnect = false; [JsonProperty("Delay on connect (seconds)")] public float ConnectDelay = 3f; [JsonProperty("Require permission (majestattopinfo.use)")] public bool RequirePermission = false; [JsonProperty("Integrate with MajestatTopCore (if present)")] public bool IntegrateWithCore = true; [JsonProperty("Integrate with MajestatTopWeb (if present and version OK)")] public bool WebIntegration = true; [JsonProperty("Allow load when Web version mismatch (WebIntegration=true required, set false to skip web tab only)")] public bool AllowLoadOnWebMismatch = false; [JsonProperty("Check for updates")] public bool CheckUpdates = true; [JsonProperty("Tabs", ObjectCreationHandling = ObjectCreationHandling.Replace)] public List Tabs = new List(); [JsonProperty("Broadcast")] public BroadcastConfig Broadcast = new BroadcastConfig(); } private class BroadcastConfig { [JsonProperty("Enabled")] public bool Enabled = true; [JsonProperty("Interval minutes")] public float IntervalMinutes = 15f; [JsonProperty("Tag (e.g. [INFO])")] public string Tag = "[INFO]"; [JsonProperty("Tag color (#RRGGBB)")] public string TagColor = "#2ec4f0"; [JsonProperty("Nick (sender name)")] public string Nick = "Serwer"; [JsonProperty("Nick color (#RRGGBB)")] public string NickColor = "#ffffff"; [JsonProperty("Message text color (#RRGGBB)")] public string TextColor = "#cccccc"; [JsonProperty("Avatar SteamID (empty = default Rust icon, enter SteamID64 for custom avatar)")] public string AvatarSteamId = ""; [JsonProperty("Messages (cycled in order)", ObjectCreationHandling = ObjectCreationHandling.Replace)] public List Messages = new List { "Wpisz /help aby zobaczyć informacje o serwerze.", "Sprawdź rankingi wpisując /top.", "Pamiętaj o zapoznaniu się z regulaminem (/help → Regulamin).", "Dołącz do naszego Discorda! Link znajdziesz w /help → Kontakt." }; } private ConfigData _cfg; // Ścieżka do pliku cfg private string CfgPath => System.IO.Path.Combine(Interface.Oxide.ConfigDirectory, "MajestatTopInfo.json"); protected override void LoadConfig() { // WALIDACJA PRZED base.LoadConfig() — base może wywołać LoadDefaultConfig() // i nadpisać plik jeśli JSON jest niepoprawny string cfgPath = CfgPath; if (System.IO.File.Exists(cfgPath)) { string raw = ""; try { raw = System.IO.File.ReadAllText(cfgPath, System.Text.Encoding.UTF8); } catch (Exception ex) { PrintError($"[MajestatTopInfo] Cannot read cfg file: {ex.Message}"); _cfg = DefaultConfig(); return; // nie wywołuj base ani SaveConfig } if (!string.IsNullOrWhiteSpace(raw)) { try { Newtonsoft.Json.Linq.JToken.Parse(raw); // tylko walidacja składni } catch (Newtonsoft.Json.JsonException ex) { PrintError("==============================================="); PrintError("[MajestatTopInfo] CFG FILE SYNTAX ERROR!"); PrintError($" Details: {ex.Message}"); PrintError($" File: {cfgPath}"); PrintError(" File will NOT be overwritten - fix it manually."); PrintError(" Plugin will not load while the cfg is corrupt."); PrintError("==============================================="); // Ustaw domyślny w pamięci ale NIE zapisuj na dysk _cfg = DefaultConfig(); // Rzuć wyjątek — Oxide odrzuci ładowanie pluginu throw new Exception($"MajestatTopInfo.json syntax error: {ex.Message}"); } } } // JSON poprawny (lub plik nie istnieje) — teraz dopiero wywołaj base base.LoadConfig(); try { _cfg = Config.ReadObject(); if (_cfg == null) { PrintWarning("[MajestatTopInfo] Empty cfg - generating default."); _cfg = DefaultConfig(); SaveConfig(); // nowy plik — zapisz } else { SaveConfig(); // uzupełnij nowe pola } } catch (Exception ex) { PrintError($"[MajestatTopInfo] Cfg deserialization error: {ex.Message}"); PrintError(" File will NOT be overwritten - plugin uses default cfg in memory."); _cfg = DefaultConfig(); // NIE SaveConfig() — nie nadpisuj uszkodzonego pliku } } protected override void LoadDefaultConfig() { // Wywoływane przez Oxide gdy plik nie istnieje — generuj domyślny _cfg = DefaultConfig(); } protected override void SaveConfig() => Config.WriteObject(_cfg); private ConfigData DefaultConfig() => new ConfigData { Title = "INFORMACJE O SERWERZE", TitleColor = "0.3 0.85 1 1", TabBarWidth = 0.21f, DefaultTab = "rules", ShowOnConnect = true, ShowEveryConnect = false, ConnectDelay = 3f, RequirePermission = false, IntegrateWithCore = true, WebIntegration = true, AllowLoadOnWebMismatch = false, CheckUpdates = true, Commands = new List { "help", "info", "rules", "regulamin" }, Tabs = new List { new InfoTab { Id = "rules", Label = "Regulamin", Icon = "📋", Color = "0.2 0.6 0.9", Lines = new List { "[size=15][b][color=#4dcfff]REGULAMIN SERWERA[/color][/b][/size]", "", "[b]§1. Ogólne zasady[/b]", "• Szanuj innych graczy i administrację.", "• Zakaz reklamy innych serwerów.", "", "[b]§2. Rozgrywka[/b]", "• Raidowanie i KOS dozwolone — to Rust!", "• Zakaz cheaterów i exploitów.", "• Sojusze max [b]4 graczy[/b].", "", "[b]§3. Konsekwencje[/b]", "Złamanie regulaminu = [color=#ff4444]ban[/color]." } }, new InfoTab { Id = "howtoplay", Label = "Jak grać", Icon = "⚔", Color = "0.2 0.75 0.35", Lines = new List { "[size=15][b][color=#66ee88]JAK GRAĆ[/color][/b][/size]", "", "[b]Komendy:[/b]", "/top — rankingi i statystyki", "/help — to okno", "", "[b]XP:[/b]", "Za aktywność zdobywasz [color=#ffcc00]punkty XP[/color].", "Sprawdź /top → XP INFO." } }, new InfoTab { Id = "contact", Label = "Kontakt", Icon = "✉", Color = "0.75 0.45 0.2", Lines = new List { "[size=15][b][color=#ffaa55]KONTAKT[/color][/b][/size]", "", "[url=https://discord.gg/twoj-serwer]Discord — kliknij tutaj[/url]", "", "[color=#aaaaaa]Reagujemy w ciągu 24h.[/color]" } }, new InfoTab { Id = "vip", Label = "VIP / Sklep", Icon = "★", Color = "0.8 0.7 0.1", Lines = new List { "[size=15][b][color=#ffdd44]PAKIETY VIP[/color][/b][/size]", "", "[color=#ffdd44][b]VIP SILVER — 15 PLN/mies.[/b][/color]", "• Zestaw startowy, priorytetowy slot", "", "[color=#aaccff][b]VIP GOLD — 30 PLN/mies.[/b][/color]", "• Wszystko z Silver + /home", "", "[url=https://sklep.twojserwer.pl]Przejdź do sklepu[/url]" } } } }; // ═══════════════════════════════════════════════════════════════ // WERSJE // ═══════════════════════════════════════════════════════════════ private static bool CheckVer(Oxide.Core.VersionNumber v, int ma, int mi, int pa) => v.Major > ma || (v.Major == ma && (v.Minor > mi || (v.Minor == mi && v.Patch >= pa))); // ═══════════════════════════════════════════════════════════════ // DANE GRACZY (dismissed) // ═══════════════════════════════════════════════════════════════ private void LoadDismissed() { try { var d = Interface.Oxide.DataFileSystem.ReadObject>("MajestatTopInfo_dismissed"); if (d != null) _dismissed = new HashSet(d); } catch { _dismissed = new HashSet(); } } private void SaveDismissed() => Interface.Oxide.DataFileSystem.WriteObject("MajestatTopInfo_dismissed", new List(_dismissed)); // ═══════════════════════════════════════════════════════════════ // INIT // ═══════════════════════════════════════════════════════════════ void OnServerInitialized() { permission.RegisterPermission(PermUse, this); permission.RegisterPermission(PermAdmin, this); LoadDismissed(); RegisterCommands(); // Opóźnienie 2s — Core potrzebuje czasu na InitDatabase + MigrateFromLegacy // zanim zacznie przyjmować rejestracje modułów przez API. // Eliminuje fałszywy "Tryb: STANDALONE" przy pierwszym starcie serwera. timer.Once(2.0f, () => { DetermineMode(); if (_cfg.CheckUpdates) webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); StartBroadcast(); }); } void OnPluginLoaded(Plugin plugin) { if (plugin == null) return; if (plugin.Name == "MajestatTopCore" || plugin.Name == "MajestatTopWeb") { // Delay 0.5s — daje Core czas na uruchomienie OnServerInitialized // i ustawienie _autoReloadActive=true zanim DetermineMode wydrukuje cokolwiek timer.Once(0.5f, DetermineMode); } } void OnPluginUnloaded(Plugin plugin) { if (plugin == null) return; if (plugin.Name == "MajestatTopCore" || plugin.Name == "MajestatTopWeb") { _registered = false; _mode = WorkMode.Standalone; // cicho — bez MLog, Core drukuje status } } void Unload() { _broadcastTimer?.Destroy(); _broadcastTimer = null; foreach (var c in _registeredCmds) cmd.RemoveChatCommand(c, this); _registeredCmds.Clear(); SaveDismissed(); foreach (var p in BasePlayer.activePlayerList) if (_activePanels.Contains(p.userID)) CloseUI(p); _activePanels.Clear(); } private void RegisterCommands() { _registeredCmds.Clear(); foreach (var command in _cfg.Commands) { string c = command.Replace("/", "").Trim().ToLower(); if (string.IsNullOrEmpty(c)) continue; cmd.AddChatCommand(c, this, "CmdHelp"); _registeredCmds.Add(c); } } // ═══════════════════════════════════════════════════════════════ // USTALANIE TRYBU PRACY // ═══════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════ // BROADCAST // ═══════════════════════════════════════════════════════════════ private void StartBroadcast() { _broadcastTimer?.Destroy(); if (!_cfg.Broadcast.Enabled || _cfg.Broadcast.Messages == null || _cfg.Broadcast.Messages.Count == 0 || _cfg.Broadcast.IntervalMinutes <= 0) return; float interval = _cfg.Broadcast.IntervalMinutes * 60f; _broadcastTimer = timer.Every(interval, SendBroadcast); MLog(1, $"[Broadcast] Active - every {_cfg.Broadcast.IntervalMinutes} min, " + $"{_cfg.Broadcast.Messages.Count} messages."); } private void SendBroadcast() { var bc = _cfg.Broadcast; if (!bc.Enabled || bc.Messages == null || bc.Messages.Count == 0) return; string raw = bc.Messages[_broadcastIndex % bc.Messages.Count]; _broadcastIndex++; // Konwertuj BBCode wiadomości na Rust Steam Rich Text string text = BbCodeToChat(raw); // Prefiks: [TAG] Nick: string formatted = ""; if (!string.IsNullOrEmpty(bc.Tag)) formatted += $"{bc.Tag} "; if (!string.IsNullOrEmpty(bc.Nick)) formatted += $"{bc.Nick}: "; formatted += $"{text}"; ulong avatarId = 0; if (!string.IsNullOrEmpty(bc.AvatarSteamId)) ulong.TryParse(bc.AvatarSteamId.Trim(), out avatarId); foreach (var player in BasePlayer.activePlayerList) Player.Message(player, formatted, avatarId); MLog(1, $"[Broadcast] Sent ({_broadcastIndex}): {raw}"); } // Konwertuje BBCode → Rust Steam Rich Text (używany przez chat) // [color=#hex]...[/color] → ... // [b]...[/b] → ... // [i]...[/i] → ... // [size=N]...[/size] → ... // [url=https://...]txt → txt (link) // (URL w chacie nie jest klikalny — pokazujemy w nawiasie) private string BbCodeToChat(string s) { if (string.IsNullOrEmpty(s)) return s; // [b] i [i] s = s.Replace("[b]", "").Replace("[/b]", ""); s = s.Replace("[i]", "").Replace("[/i]", ""); // [color=#hex]...[/color] int ci = s.IndexOf("[color="); while (ci >= 0) { int ce = s.IndexOf("]", ci); if (ce < 0) break; string hex = s.Substring(ci + 7, ce - ci - 7).Trim(); // Upewnij się że hex zaczyna się od # (Steam wymaga #RRGGBB) if (!hex.StartsWith("#")) hex = "#" + hex; s = s.Substring(0, ci) + $"" + s.Substring(ce + 1); // Replace jest case-sensitive — tagi zawsze lowercase z cfg s = s.Replace("[/color]", ""); ci = s.IndexOf("[color="); } // [size=N]...[/size] int si = s.IndexOf("[size="); while (si >= 0) { int se = s.IndexOf("]", si); if (se < 0) break; string sz = s.Substring(si + 6, se - si - 6).Trim(); s = s.Substring(0, si) + $"" + s.Substring(se + 1); s = s.Replace("[/size]", ""); si = s.IndexOf("[size="); } // [url=https://...]tekst[/url] → tekst (https://...) int ui = s.IndexOf("[url="); while (ui >= 0) { int ue = s.IndexOf("]", ui); if (ue < 0) break; string url = s.Substring(ui + 5, ue - ui - 5).Trim(); int uc = s.IndexOf("[/url]", ue); string text = uc > ue ? s.Substring(ue + 1, uc - ue - 1) : url; string after = uc >= 0 ? s.Substring(uc + 6) : s.Substring(ue + 1); s = s.Substring(0, ui) + text + $" ({url})" + after; ui = s.IndexOf("[url="); } return s; } private void DetermineMode() { // Brak Core lub integracja wyłączona → standalone if (!_cfg.IntegrateWithCore || MajestatTopCore == null) { _mode = WorkMode.Standalone; _registered = false; MLog(1, "[Info] Mode: STANDALONE (Core unavailable or integration disabled)."); return; } // Core załadowany — sprawdź wersję if (!CheckVer(MajestatTopCore.Version, 3, 2, 0)) { PrintError($"[MajestatTopInfo] Requires MajestatTopCore 3.2.0+. Installed: {MajestatTopCore.Version}. " + "Przełączam na tryb STANDALONE."); _mode = WorkMode.Standalone; _registered = false; return; } // Core OK — sprawdź Web if (MajestatTopWeb == null || !_cfg.WebIntegration) { _mode = WorkMode.CoreOnly; RegisterWithCore(webEnabled: false); MLog(1, "[Info] Mode: CORE ONLY (Web unavailable or WebIntegration=false)."); MajestatTopCore?.Call("API_SetModuleExtra", "MajestatTopInfo", "Core Only"); return; } // Web załadowany — sprawdź wersję if (!CheckVer(MajestatTopWeb.Version, 1, 2, 0)) { if (!_cfg.AllowLoadOnWebMismatch) { PrintError($"[MajestatTopInfo] MajestatTopWeb {MajestatTopWeb.Version} jest za stary " + $"(wymagany 1.2.0+). Ustaw AllowLoadOnWebMismatch=true lub zaktualizuj Web. " + "Przełączam na tryb CORE ONLY."); _mode = WorkMode.CoreOnly; RegisterWithCore(webEnabled: false); } else { _mode = WorkMode.CoreOnly; RegisterWithCore(webEnabled: false); PrintWarning($"[MajestatTopInfo] Web {MajestatTopWeb.Version} < 1.2.0 - GAME INFO tab " + "unavailable in browser (AllowLoadOnWebMismatch=true)."); } return; } // Wszystko OK — pełna integracja _mode = WorkMode.Full; RegisterWithCore(webEnabled: true); MLog(1, "[Info] Mode: FULL (Core + Web - full integration)."); MajestatTopCore?.Call("API_SetModuleExtra", "MajestatTopInfo", "FULL — Core+Web"); } // ═══════════════════════════════════════════════════════════════ // REJESTRACJA W CORE // ═══════════════════════════════════════════════════════════════ private void RegisterWithCore(bool webEnabled) { if (MajestatTopCore == null) return; MajestatTopCore.Call("API_RegisterModule", new Dictionary { ["name"] = "MajestatTopInfo", ["version"] = PluginVersion, ["author"] = "Wo0t", ["description"] = "Server info panel (/help)" + (webEnabled ? " + Web tab" : "") }); var tabList = new List>(); foreach (var t in _cfg.Tabs) tabList.Add(new Dictionary { ["id"] = t.Id, ["label"] = t.Label, ["icon"] = t.Icon, ["color"] = t.Color }); MajestatTopCore.Call("API_RegisterInfoModule", new Dictionary { ["title"] = _cfg.Title, ["tabs"] = tabList, ["default"] = _cfg.DefaultTab }); _registered = true; } // ═══════════════════════════════════════════════════════════════ // HOOKS OD CORE // ═══════════════════════════════════════════════════════════════ [HookMethod("OpenEmbedded")] public void OpenEmbedded(BasePlayer player, string tabId, bool showDismiss) { if (player == null) return; var tab = FindTab(tabId) ?? (_cfg.Tabs.Count > 0 ? _cfg.Tabs[0] : null); if (tab == null) return; // firstOpen = true → nowa sesja, CORE_BG nie istnieje → tworzymy go razem z INFO_EMBED // firstOpen = false → zmiana zakładki, CORE_BG już istnieje → tylko podmieniamy INFO_EMBED // (bez recreate CORE_BG = kursor nie skacze na środek) // _activePanels jest synchronizowane przez OnInfoPanelClosed gdy Core zamknie panel bool firstOpen = !_activePanels.Contains(player.userID); _activePanels.Add(player.userID); var c = new CuiElementContainer(); if (firstOpen) { // Pierwsze otwarcie — CORE_BG i INFO_EMBED w jednym pakiecie (eliminuje "Unknown Parent") c.Add(new CuiPanel { Image = { Color = "0 0 0 0" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }, CursorEnabled = true }, "Overlay", CORE_BG); } c.Add(new CuiPanel { Image = { Color = "0.05 0.05 0.05 0.99" }, RectTransform = { AnchorMin = "0.03 0.1", AnchorMax = "0.97 0.9" }, CursorEnabled = false }, CORE_BG, INFO_EMBED); BuildInfoPanel(ref c, INFO_EMBED, player, tab, tabId); if (firstOpen) CuiHelper.DestroyUi(player, CORE_BG); // niszczy stary CORE_BG jeśli istniał else CuiHelper.DestroyUi(player, INFO_EMBED); // tylko podmień treść, CORE_BG zostaje CuiHelper.AddUi(player, c); } // Core wywołuje ten hook gdy zamyka panel przez majestattopcore.closeinfo // — synchronizuje _activePanels żeby firstOpen działał poprawnie przy kolejnym /help [HookMethod("OnInfoPanelClosed")] public void OnInfoPanelClosed(BasePlayer player) { if (player != null) _activePanels.Remove(player.userID); } // API dla Web — zwraca JSON string [HookMethod("API_GetInfoData")] public string API_GetInfoData() { var tabs = new List(); foreach (var t in _cfg.Tabs) tabs.Add(new { id = t.Id, label = t.Label, icon = t.Icon, color = t.Color, lines = t.Lines }); return JsonConvert.SerializeObject(new { title = _cfg.Title, tabs }); } // ═══════════════════════════════════════════════════════════════ // EVENT — POŁĄCZENIE GRACZA // ═══════════════════════════════════════════════════════════════ void OnPlayerConnected(BasePlayer player) { if (!_cfg.ShowOnConnect || player == null) return; if (!_cfg.ShowEveryConnect && _dismissed.Contains(player.UserIDString)) return; timer.Once(_cfg.ConnectDelay, () => { if (player == null || !player.IsConnected) return; if (_cfg.RequirePermission && !permission.UserHasPermission(player.UserIDString, PermUse)) return; OpenPanel(player, _cfg.DefaultTab); }); } void OnPlayerDisconnected(BasePlayer player, string reason) { if (_activePanels.Contains(player.userID)) { _activePanels.Remove(player.userID); CuiHelper.DestroyUi(player, INFO_EMBED); CuiHelper.DestroyUi(player, PANEL_MAIN); CuiHelper.DestroyUi(player, PANEL_BG); } } // ═══════════════════════════════════════════════════════════════ // KOMENDY // ═══════════════════════════════════════════════════════════════ void CmdHelp(BasePlayer player, string cmdName, string[] args) { if (player == null) return; if (_cfg.RequirePermission && !permission.UserHasPermission(player.UserIDString, PermUse)) { Player.Message(player, "Brak uprawnień."); return; } string tabId = args.Length > 0 ? args[0] : _cfg.DefaultTab; OpenPanel(player, tabId); } [ConsoleCommand("majestattopinfo.open")] void ConsoleCmdOpen(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; OpenPanel(player, arg.GetString(0, _cfg.DefaultTab)); } [ConsoleCommand("majestattopinfo.tab")] void ConsoleCmdTab(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; string tabId = arg.GetString(0, _cfg.DefaultTab); if (_mode != WorkMode.Standalone && _registered) OpenEmbedded(player, tabId, false); else OpenStandalone(player, tabId); } [ConsoleCommand("majestattopinfo.dismiss")] void ConsoleCmdDismiss(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; _dismissed.Add(player.UserIDString); SaveDismissed(); // Usuń z _activePanels → firstOpen=true w OpenEmbedded → pełny rebuild z zaznaczonym checkboxem _activePanels.Remove(player.userID); string tabId = arg.GetString(0, _cfg.DefaultTab); if (_mode != WorkMode.Standalone && _registered) MajestatTopCore?.Call("API_OpenInfoPanel", player, tabId, false); else OpenStandalone(player, tabId); } [ConsoleCommand("majestattopinfo.undismiss")] void ConsoleCmdUndismiss(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; _dismissed.Remove(player.UserIDString); SaveDismissed(); _activePanels.Remove(player.userID); string tabId = arg.GetString(0, _cfg.DefaultTab); if (_mode != WorkMode.Standalone && _registered) MajestatTopCore?.Call("API_OpenInfoPanel", player, tabId, false); else OpenStandalone(player, tabId); } [ConsoleCommand("majestattopinfo.openurl")] void ConsoleCmdOpenUrl(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; string url = arg.Args != null && arg.Args.Length > 0 ? string.Join(" ", arg.Args) : ""; if (string.IsNullOrEmpty(url)) return; // Rust nie obsługuje otwierania URL z gry. // Wyświetl link w czacie — gracz może skopiować z konsoli F1 Player.Message(player, $"\u25ba Link: {url}\n" + "(Otworz konsole F1 aby skopiowac)"); } [ConsoleCommand("majestattopinfo.close")] void ConsoleCmdClose(ConsoleSystem.Arg arg) { var player = arg.Player(); if (player == null) return; CloseUI(player); if (_mode != WorkMode.Standalone) MajestatTopCore?.Call("API_CloseInfoPanel", player); } // /topinfo reload — przeładuj konfigurację bez restartu pluginu [ChatCommand("topinfo")] void CmdTopInfo(BasePlayer player, string cmdName, string[] args) { if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin)) { Player.Message(player, "Brak uprawnień."); return; } if (args.Length == 0 || args[0].ToLower() != "reload") { string msg = "[MajestatTopInfo] Dostępne komendy:\n /topinfo reload — przeładuj konfigurację"; if (player != null) Player.Message(player, msg); else Puts(msg.Replace("","").Replace("","").Replace("","")); return; } // Przeładuj config — złap wyjątek składni JSON (nie crashuj sessji) try { LoadConfig(); } catch (Exception ex) { string err = $"[MajestatTopInfo] B\u0142\u0105d cfg \u2014 plik NIE zosta\u0142 nadpisany.\n{ex.Message}"; if (player != null) Player.Message(player, err); else PrintError($"[Reload] Cfg error: {ex.Message}"); return; // przerwij reload — stary cfg w pamięci pozostaje } // Restart broadcast z nowym interwałem/wiadomościami _broadcastTimer?.Destroy(); _broadcastTimer = null; _broadcastIndex = 0; StartBroadcast(); // Odśwież rejestrację w Core (nowe zakładki/tytuł mogły się zmienić) if (_mode != WorkMode.Standalone && MajestatTopCore != null) { _registered = false; RegisterWithCore(_mode == WorkMode.Full); } string ok = "[MajestatTopInfo] Konfiguracja przeładowana ✓"; if (player != null) Player.Message(player, ok); MLog(1, "[Reload] Config reloaded."); } // /broadcast — ręczne wysłanie broadcastu (admin) [ChatCommand("broadcast")] void CmdBroadcast(BasePlayer player, string cmdName, string[] args) { if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin)) { Player.Message(player, "Brak uprawnień."); return; } if (args.Length > 0) { // /broadcast — wyślij konkretną wiadomość od razu var bc = _cfg.Broadcast; string text = string.Join(" ", args); string formatted = ""; if (!string.IsNullOrEmpty(bc.Tag)) formatted += $"{bc.Tag} "; if (!string.IsNullOrEmpty(bc.Nick)) formatted += $"{bc.Nick}: "; formatted += $"{text}"; ulong avatarId2 = 0; if (!string.IsNullOrEmpty(_cfg.Broadcast.AvatarSteamId)) ulong.TryParse(_cfg.Broadcast.AvatarSteamId.Trim(), out avatarId2); foreach (var p in BasePlayer.activePlayerList) Player.Message(p, formatted, avatarId2); MLog(1, $"[Broadcast] Admin sent manually: {text}"); } else { // /broadcast bez argumentów — wyślij następną wiadomość z listy SendBroadcast(); } if (player != null) Player.Message(player, "Broadcast wysłany."); } // ═══════════════════════════════════════════════════════════════ // OTWIERANIE PANELU — router // ═══════════════════════════════════════════════════════════════ private void OpenPanel(BasePlayer player, string tabId) { if (_mode != WorkMode.Standalone && _registered && MajestatTopCore != null) MajestatTopCore.Call("API_OpenInfoPanel", player, tabId, false); else OpenStandalone(player, tabId); } // ═══════════════════════════════════════════════════════════════ // PANEL STANDALONE // ═══════════════════════════════════════════════════════════════ private void OpenStandalone(BasePlayer player, string tabId) { var tab = FindTab(tabId) ?? (_cfg.Tabs.Count > 0 ? _cfg.Tabs[0] : null); if (tab == null) return; var c = new CuiElementContainer(); // Pierwszy panel — PANEL_BG i PANEL_MAIN w jednym AddUi (brak Unknown Parent) bool firstOpen = !_activePanels.Contains(player.userID); if (firstOpen) { c.Add(new CuiPanel { Image = { Color = "0 0 0 0.5" }, RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }, CursorEnabled = true }, "Overlay", PANEL_BG); _activePanels.Add(player.userID); } c.Add(new CuiPanel { Image = { Color = "0.05 0.05 0.05 0.99" }, RectTransform = { AnchorMin = "0.03 0.1", AnchorMax = "0.97 0.9" }, CursorEnabled = false }, PANEL_BG, PANEL_MAIN); BuildInfoPanel(ref c, PANEL_MAIN, player, tab, tabId); if (firstOpen) CuiHelper.DestroyUi(player, PANEL_BG); else CuiHelper.DestroyUi(player, PANEL_MAIN); CuiHelper.AddUi(player, c); } // ═══════════════════════════════════════════════════════════════ // BUDOWANIE PANELU // ═══════════════════════════════════════════════════════════════ private void BuildInfoPanel(ref CuiElementContainer c, string parent, BasePlayer player, InfoTab tab, string activeTabId) { bool dismissed = _dismissed.Contains(player.UserIDString); // ── Tytuł ──────────────────────────────────────────────── c.Add(new CuiLabel { Text = { Text = _cfg.Title, FontSize = 24, Font = "robotocondensed-bold.ttf", Align = TextAnchor.MiddleLeft, Color = _cfg.TitleColor }, RectTransform = { AnchorMin = "0.03 0.925", AnchorMax = "0.65 0.995" } }, parent); // Separator (kolor aktywnej zakładki) c.Add(new CuiPanel { Image = { Color = $"{TabColor(tab.Color)} 0.7" }, RectTransform = { AnchorMin = "0.03 0.917", AnchorMax = "0.97 0.921" } }, parent); // ── Lewy pasek zakładek ─────────────────────────────────── BuildLeftTabBar(ref c, parent, activeTabId); // ── Treść ───────────────────────────────────────────────── BuildRightContent(ref c, parent, tab, activeTabId); // ── Dolny pasek ────────────────────────────────────────── c.Add(new CuiPanel { Image = { Color = "0.04 0.04 0.06 1" }, RectTransform = { AnchorMin = "0.03 0.005", AnchorMax = "0.97 0.077" } }, parent); c.Add(new CuiPanel { Image = { Color = "0.12 0.12 0.18 1" }, RectTransform = { AnchorMin = "0.03 0.074", AnchorMax = "0.97 0.077" } }, parent); // ZAMKNIJ string closeCmd = (_mode != WorkMode.Standalone && _registered) ? "majestattopcore.closeinfo" : "majestattopinfo.close"; c.Add(new CuiButton { Button = { Command = closeCmd, Color = "0.65 0.12 0.12 0.9" }, Text = { Text = "✕ ZAMKNIJ", FontSize = 13, Font = "robotocondensed-bold.ttf", Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }, RectTransform = { AnchorMin = "0.876 0.012", AnchorMax = "0.967 0.068" } }, parent); // Checkbox "Nie pokazuj przy starcie" // (używamy 'dismissed' zadeklarowanego na początku metody) string toggleCmd = dismissed ? $"majestattopinfo.undismiss {activeTabId}" : $"majestattopinfo.dismiss {activeTabId}"; string checkIcon = dismissed ? "☑" : "☐"; string checkBg = dismissed ? "0.10 0.28 0.45 0.95" : "0.10 0.10 0.14 0.9"; string checkBdr = dismissed ? "0.20 0.55 0.85 1" : "0.22 0.22 0.30 1"; string checkTxt = dismissed ? "0.55 0.88 1.00 1" : "0.42 0.42 0.50 1"; string checkFont = dismissed ? "robotocondensed-bold.ttf" : "robotocondensed-regular.ttf"; c.Add(new CuiPanel { Image = { Color = checkBdr }, RectTransform = { AnchorMin = "0.636 0.010", AnchorMax = "0.872 0.070" } }, parent); c.Add(new CuiButton { Button = { Command = toggleCmd, Color = checkBg }, Text = { Text = checkIcon + " Nie pokazuj przy starcie", FontSize = 12, Font = checkFont, Align = TextAnchor.MiddleCenter, Color = checkTxt }, RectTransform = { AnchorMin = "0.637 0.012", AnchorMax = "0.871 0.068" } }, parent); } // ───────────────────────────────────────────────────────────── // LEWY PASEK ZAKŁADEK // ───────────────────────────────────────────────────────────── private void BuildLeftTabBar(ref CuiElementContainer c, string parent, string activeId) { float tw = _cfg.TabBarWidth; float left = 0.03f; float right = left + tw; float top = 0.912f; // górna krawędź paska (pod separatorem tytułu) float bot = 0.082f; float tabH = 0.067f; float gap = 0.005f; float katH = 0.034f; // wysokość nagłówka KATEGORIE wewnątrz paska float startY = top - katH - gap; // pierwsze zakładki startują poniżej KATEGORIE // Tło kolumny (obejmuje KATEGORIE + zakładki) c.Add(new CuiPanel { Image = { Color = "0.07 0.07 0.10 1" }, RectTransform = { AnchorMin = $"{left:F3} {bot:F3}", AnchorMax = $"{right:F3} {top:F3}" } }, parent); // Nagłówek "KATEGORIE" — wewnątrz paska, tuż pod separatorem tytułu c.Add(new CuiLabel { Text = { Text = "— KATEGORIE —", FontSize = 9, Font = "robotocondensed-bold.ttf", Align = TextAnchor.MiddleCenter, Color = "0.50 0.52 0.65 1" }, RectTransform = { AnchorMin = $"{left:F3} {top - katH:F3}", AnchorMax = $"{right:F3} {top:F3}" } }, parent); // Cienki separator pod nagłówkiem KATEGORIE c.Add(new CuiPanel { Image = { Color = "0.12 0.13 0.20 1" }, RectTransform = { AnchorMin = $"{left + 0.01f:F3} {top - katH - 0.002f:F3}", AnchorMax = $"{right - 0.01f:F3} {top - katH:F3}" } }, parent); // Separator pionowy c.Add(new CuiPanel { Image = { Color = "0.13 0.15 0.22 1" }, RectTransform = { AnchorMin = $"{right:F3} {bot:F3}", AnchorMax = $"{right + 0.003f:F3} {top:F3}" } }, parent); bool embedded = (_mode != WorkMode.Standalone && _registered); string tabCmd = embedded ? "majestattopcore.infotab" : "majestattopinfo.tab"; for (int i = 0; i < _cfg.Tabs.Count; i++) { var t = _cfg.Tabs[i]; bool act = t.Id == activeId; float y1 = startY - i * (tabH + gap); float y0 = y1 - tabH; if (y0 < bot) break; // Aktywny — pionowy pasek 3px po lewej if (act) c.Add(new CuiPanel { Image = { Color = $"{TabColor(t.Color)} 1" }, RectTransform = { AnchorMin = $"{left:F3} {y0:F3}", AnchorMax = $"{left + 0.003f:F3} {y1:F3}" } }, parent); c.Add(new CuiButton { Button = { Command = $"{tabCmd} {t.Id}", Color = act ? $"{TabColor(t.Color)} 0.22" : "0.09 0.09 0.13 0.9" }, Text = { Text = $"{t.Icon} {t.Label}", FontSize = act ? 13 : 12, Font = act ? "robotocondensed-bold.ttf" : "robotocondensed-regular.ttf", Align = TextAnchor.MiddleLeft, Color = act ? "1 1 1 1" : "0.60 0.60 0.66 1" }, RectTransform = { AnchorMin = $"{left + 0.003f:F3} {y0:F3}", AnchorMax = $"{right - 0.002f:F3} {y1:F3}" } }, parent); } } // ───────────────────────────────────────────────────────────── // PRAWA STRONA — TREŚĆ // ───────────────────────────────────────────────────────────── private void BuildRightContent(ref CuiElementContainer c, string parent, InfoTab tab, string activeId) { float tw = _cfg.TabBarWidth; float left = 0.03f + tw + 0.012f; float right = 0.97f; float top = 0.912f; float bot = 0.082f; // Nagłówek zakładki c.Add(new CuiLabel { Text = { Text = $"{tab.Icon} {tab.Label.ToUpper()}", FontSize = 15, Font = "robotocondensed-bold.ttf", Align = TextAnchor.MiddleLeft, Color = $"{TabColor(tab.Color)} 1" }, RectTransform = { AnchorMin = $"{left:F3} {top - 0.048f:F3}", AnchorMax = $"{right:F3} {top:F3}" } }, parent); c.Add(new CuiPanel { Image = { Color = $"{TabColor(tab.Color)} 0.28" }, RectTransform = { AnchorMin = $"{left:F3} {top - 0.051f:F3}", AnchorMax = $"{right:F3} {top - 0.048f:F3}" } }, parent); RenderLines(ref c, parent, tab.Lines, left, right, top - 0.056f, bot); } // ───────────────────────────────────────────────────────────── // RENDERER LINII // ───────────────────────────────────────────────────────────── private void RenderLines(ref CuiElementContainer c, string parent, List lines, float x0, float x1, float startY, float minY) { float lineH = 0.050f; float baseSize = 12; float curY = startY; foreach (var rawLine in lines) { if (curY - lineH < minY) break; if (string.IsNullOrEmpty(rawLine)) { curY -= lineH * 0.50f; continue; } // [img]url[/img] if (rawLine.TrimStart().StartsWith("[img]") && rawLine.Contains("[/img]")) { int si = rawLine.IndexOf("[img]") + 5; int ei = rawLine.IndexOf("[/img]"); if (si > 4 && ei > si) { string url = rawLine.Substring(si, ei - si).Trim(); float imgH = lineH * 3.5f; if (curY - imgH < minY) break; float iy0 = curY - imgH; float imgW = x0 + (x1 - x0) * 0.65f; c.Add(new CuiPanel { Image = { Color = "0.08 0.08 0.12 1" }, RectTransform = { AnchorMin = $"{x0:F3} {iy0:F3}", AnchorMax = $"{imgW:F3} {curY:F3}" } }, parent); c.Add(new CuiElement { Parent = parent, Components = { new CuiRawImageComponent { Url = url, Color = "1 1 1 1" }, new CuiRectTransformComponent { AnchorMin = $"{x0+0.003f:F3} {iy0+0.005f:F3}", AnchorMax = $"{imgW-0.003f:F3} {curY-0.005f:F3}" } } }); curY -= imgH + lineH * 0.25f; continue; } } var parsed = ParseLine(rawLine); // Rozmiar czcionki: tagi [size=N] są relatywne do baseSize // Brak tagu = baseSize, [size=15] = 15, [size=12] = 12 int fSize = parsed.Size == 0 ? (int)baseSize : parsed.Size; float thisH = lineH; if (fSize > baseSize) thisH = lineH * (fSize / baseSize); if (curY - thisH < minY) break; float y0 = curY - thisH; string font = parsed.Bold ? "robotocondensed-bold.ttf" : "robotocondensed-regular.ttf"; // Kolor: pobrany z tagu [color=] — jeśli brak, domyślny jasny // Web domyślnie: var(--color-text) ≈ biały/jasnoszary string col = parsed.Color ?? "0.92 0.92 0.93 1"; if (!string.IsNullOrEmpty(parsed.Url)) { c.Add(new CuiButton { Button = { Command = $"majestattopinfo.openurl {parsed.Url}", Color = "0 0 0 0" }, Text = { Text = parsed.Text, FontSize = fSize, Font = font, Align = TextAnchor.MiddleLeft, Color = "0.3 0.78 1 1" }, RectTransform = { AnchorMin = $"{x0:F3} {y0:F3}", AnchorMax = $"{x1:F3} {curY:F3}" } }, parent); // Podkreślenie linku c.Add(new CuiPanel { Image = { Color = "0.3 0.78 1 0.45" }, RectTransform = { AnchorMin = $"{x0:F3} {y0:F3}", AnchorMax = $"{x0+(x1-x0)*0.50f:F3} {y0+0.003f:F3}" } }, parent); } else { c.Add(new CuiLabel { Text = { Text = parsed.Text, FontSize = fSize, Font = font, Align = TextAnchor.MiddleLeft, Color = col }, RectTransform = { AnchorMin = $"{x0:F3} {y0:F3}", AnchorMax = $"{x1:F3} {curY:F3}" } }, parent); } curY -= thisH; } } // ───────────────────────────────────────────────────────────── // BBCODE PARSER // ───────────────────────────────────────────────────────────── private struct ParsedLine { public string Text,Color,Url; public bool Bold; public int Size; } // Konwertuje kolor zakładki: akceptuje "#RRGGBB" lub "R G B" (float 0-1) // Zwraca zawsze "R G B" (float) — gotowe do użycia w CUI private string TabColor(string raw) { if (string.IsNullOrEmpty(raw)) return "0.2 0.6 0.9"; raw = raw.Trim(); if (raw.StartsWith("#")) { // HEX → float RGB string hex = raw.TrimStart('#'); if (hex.Length == 3) hex = "" + hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; if (hex.Length >= 6) { try { float r = Convert.ToInt32(hex.Substring(0,2),16)/255f; float g = Convert.ToInt32(hex.Substring(2,2),16)/255f; float b = Convert.ToInt32(hex.Substring(4,2),16)/255f; return $"{r:F3} {g:F3} {b:F3}"; } catch { } } return "0.2 0.6 0.9"; } // Już float — zwróć jak jest return raw; } private ParsedLine ParseLine(string raw) { var r = new ParsedLine { Text = raw, Bold = false, Size = 0 }; // 0 = brak tagu [size] string s = raw; bool b = false; s = StripTag(s,"b",ref b); r.Bold = b; int fs = 0; s = ExtractSize(s,ref fs); r.Size = fs; string col = null; s = ExtractColor(s,ref col); r.Color = col; string url = null; s = ExtractUrl(s,ref url); r.Url = url; r.Text = StripTags(s); return r; } private string StripTag(string s,string tag,ref bool f) { string o=$"[{tag}]",c=$"[/{tag}]"; if(!s.Contains(o))return s; f=true; return s.Replace(o,"").Replace(c,""); } private string ExtractSize(string s,ref int sz) { int i=s.IndexOf("[size="); if(i<0)return s; int e=s.IndexOf("]",i); if(e<0)return s; string v=s.Substring(i+6,e-i-6); if(int.TryParse(v,out int p))sz=p; return s.Substring(0,i)+s.Substring(e+1).Replace("[/size]",""); } private string ExtractColor(string s,ref string co) { int i=s.IndexOf("[color="); if(i<0)return s; int e=s.IndexOf("]",i); if(e<0)return s; co=HexRgba(s.Substring(i+7,e-i-7).Trim()); return s.Substring(0,i)+s.Substring(e+1).Replace("[/color]",""); } private string ExtractUrl(string s,ref string u) { int i=s.IndexOf("[url="); if(i<0)return s; int e=s.IndexOf("]",i); if(e<0)return s; string url=s.Substring(i+5,e-i-5).Trim(); int ce=s.IndexOf("[/url]",e); string t=ce>e?s.Substring(e+1,ce-e-1):url; u=url; return ce>=0?s.Substring(0,i)+t+s.Substring(ce+6):s.Substring(0,i)+t; } private string StripTags(string s) { int w=0; while(s.Contains("[")&&s.Contains("]")&&w++<20) { int a=s.IndexOf('['),b=s.IndexOf(']',a); if(a<0||b<0)break; s=s.Substring(0,a)+s.Substring(b+1); } return s; } private string HexRgba(string hex) { hex=hex.TrimStart('#'); try { if(hex.Length>=6){ float r=Convert.ToInt32(hex.Substring(0,2),16)/255f, g=Convert.ToInt32(hex.Substring(2,2),16)/255f, b=Convert.ToInt32(hex.Substring(4,2),16)/255f, a=hex.Length>=8?Convert.ToInt32(hex.Substring(6,2),16)/255f:1f; return $"{r:F3} {g:F3} {b:F3} {a:F3}"; } var pts=hex.Split(' '); if(pts.Length>=3)return hex; } catch{} return "0.90 0.90 0.92 1"; } // ───────────────────────────────────────────────────────────── // ZAMKNIĘCIE UI // ───────────────────────────────────────────────────────────── private void CloseUI(BasePlayer player) { _activePanels.Remove(player.userID); CuiHelper.DestroyUi(player, INFO_EMBED); CuiHelper.DestroyUi(player, PANEL_MAIN); CuiHelper.DestroyUi(player, PANEL_BG); } private InfoTab FindTab(string id) { foreach(var t in _cfg.Tabs) if(t.Id==id)return t; return null; } // ───────────────────────────────────────────────────────────── // UPDATE CHECKER // ───────────────────────────────────────────────────────────── void OnMajestatUpdateCheck() { if (_cfg.CheckUpdates) webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void OnUpdateResponse(int code, string response) { const string pn = "MajestatTopInfo"; if (code == 0 || code >= 400 || string.IsNullOrEmpty(response)) { MLog(1, $"[Update] Update check failed - server unavailable (HTTP {code})."); MajestatTopCore?.Call("API_ReportUpdateResult", pn, null, null); return; } try { var d = JsonConvert.DeserializeObject>(response); string lat = d != null && d.ContainsKey("version") ? d["version"].Trim() : null; if (lat != null && IsNewer(lat, PluginVersion)) { string url = d.ContainsKey("url") ? d["url"] : PluginUpdateUrl; MLog(2, $"[Update] Newer version available ({lat}) - your version ({PluginVersion})"); MajestatTopCore?.Call("API_ReportUpdateResult", pn, lat, url); } else { MLog(1, $"[Update] Up to date ({PluginVersion}) - no updates."); MajestatTopCore?.Call("API_ReportUpdateResult", pn, null, null); } } catch (Exception ex) { MLog(1, $"[Update] Error: {ex.Message}"); MajestatTopCore?.Call("API_ReportUpdateResult", pn, null, null); } } private bool IsNewer(string l, string c) { try { string lc = l != null && l.Contains("-") ? l.Substring(0, l.IndexOf('-')) : l; string cc = c != null && c.Contains("-") ? c.Substring(0, c.IndexOf('-')) : c; var la = Array.ConvertAll(lc.Split('.'), int.Parse); var ca = Array.ConvertAll(cc.Split('.'), int.Parse); int len = Math.Max(la.Length, ca.Length); for (int i = 0; i < len; i++) { int lv = i < la.Length ? la[i] : 0; int cv = i < ca.Length ? ca[i] : 0; if (lv > cv) return true; if (lv < cv) return false; } return false; } catch { return false; } } } }