using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Threading; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Plugins; /* * MajestatTopWeb 1.0.2 * ===================== * Web dashboard dla MajestatTopCore. * WYMAGA: MajestatTopCore 3.0.1+ * * TRYBY: "json" | "http" | "both" * ENDPOINTY: * GET / — strona glowna z rankingami * GET /player/{steamid} — profil gracza * GET /api/server — JSON info o serwerze * GET /api/tabs — JSON lista zakladek * GET /api/all — JSON wszystkie dane * GET /api/top/{tab} — JSON top dla zakladki * GET /api/top/{tab}/fame — JSON Hall of Fame * GET /api/player/{id} — JSON profil gracza */ namespace Oxide.Plugins { [Info("MajestatTopWeb", "Wo0t", "1.0.2")] [Description("Web dashboard for MajestatTopCore.")] class MajestatTopWeb : RustPlugin { [PluginReference] Plugin MajestatTopCore; private const string PluginVersion = "1.0.2"; private const string PluginUpdateUrl = "http://download.koldrix.com/rust/plugins/MajestatTopWeb/version.json"; private ConfigData _cfg; private HttpListener _listener; private Thread _thread; private bool _running = false; private int _updateIntervalMinutes = 60; // ── Config ─────────────────────────────────────────────────── private class ConfigData { [JsonProperty("Web Mode (json, http, both)")] public string WebMode = "both"; [JsonProperty("HTTP Port")] public int Port = 28020; [JsonProperty("API Key (empty = no auth)")] public string ApiKey = ""; [JsonProperty("JSON Export Interval (seconds)")] public int JsonExportInterval = 30; [JsonProperty("Leaderboard rows per tab")] public int LeaderboardRows = 10; [JsonProperty("Check for updates on startup")] public bool CheckUpdates = true; [JsonProperty("Wipe Schedule (e.g. Co tydzien w czwartek)")] public string WipeSchedule = ""; [JsonProperty("Server Type (e.g. Vanilla, Modded, PvE)")] public string ServerType = "Vanilla"; [JsonProperty("Table Number Format (none, K, M, KM)")] public string TableNumberFormat = "KM"; [JsonProperty("Table Number Decimal Places")] public int TableNumberDecimals = 1; [JsonProperty("Profile XP Number Format (none, K, M, KM)")] public string ProfileXpFormat = "KM"; [JsonProperty("Profile XP Decimal Places")] public int ProfileXpDecimals = 2; } protected override void LoadDefaultConfig() { _cfg = new ConfigData(); SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { _cfg = Config.ReadObject(); } catch { LoadDefaultConfig(); } } protected override void SaveConfig() => Config.WriteObject(_cfg); // ── Init / Unload ──────────────────────────────────────────── void OnServerInitialized() { StartUpdateChecker(); if (MajestatTopCore == null) { PrintWarning("MajestatTopCore nie zaladowany!"); return; } Start(); } void OnPluginLoaded(Plugin p) { if (p?.Name == "MajestatTopCore") Start(); } void OnPluginUnloaded(Plugin p) { if (p?.Name == "MajestatTopCore") StopHttp(); } void Unload() { StopHttp(); } private void Start() { string mode = (_cfg.WebMode ?? "both").ToLower(); if (mode == "http" || mode == "both") StartHttp(); if (mode == "json" || mode == "both") { ExportJson(); timer.Every(_cfg.JsonExportInterval, ExportJson); } Puts("Uruchomiony. Tryb: " + _cfg.WebMode.ToUpper()); } // ── HTTP Server ────────────────────────────────────────────── private void StartHttp() { if (_running) return; try { _listener = new HttpListener(); _listener.Prefixes.Add("http://*:" + _cfg.Port + "/"); _listener.Start(); _running = true; _thread = new Thread(HttpLoop) { IsBackground = true }; _thread.Start(); Puts("HTTP serwer na porcie " + _cfg.Port + "."); } catch (Exception ex) { PrintError("HTTP start error: " + ex.Message); } } private void StopHttp() { _running = false; try { _listener?.Stop(); } catch { } _listener = null; } private void HttpLoop() { while (_running) { try { var ctx = _listener.GetContext(); ThreadPool.QueueUserWorkItem(_ => HandleRequest(ctx)); } catch { if (_running) Thread.Sleep(100); } } } private void HandleRequest(HttpListenerContext ctx) { var req = ctx.Request; var resp = ctx.Response; try { resp.AddHeader("Access-Control-Allow-Origin", "*"); resp.AddHeader("Access-Control-Allow-Headers", "X-Api-Key,Content-Type"); if (req.HttpMethod == "OPTIONS") { resp.StatusCode = 204; resp.Close(); return; } if (req.HttpMethod != "GET") { SendJson(resp, 405, new { error = "Method not allowed" }); return; } if (!string.IsNullOrEmpty(_cfg.ApiKey)) { string k = req.Headers["X-Api-Key"] ?? ""; if (k != _cfg.ApiKey) { SendJson(resp, 401, new { error = "Unauthorized" }); return; } } string path = req.Url.AbsolutePath.TrimEnd('/').ToLower(); if (path == "") path = "/"; string[] pts = path.Split('/'); // Strona glowna if (path == "/" || path == "/index" || path == "/index.html") { SendHtml(resp, BuildMainPage()); return; } // Profil gracza if (pts.Length == 3 && pts[1] == "player") { string html = BuildPlayerPage(pts[2]); if (html == null) { SendJson(resp, 404, new { error = "Player not found" }); return; } SendHtml(resp, html); return; } // Avatar gracza — pobierz z Steam i przekieruj if (pts.Length == 3 && pts[1] == "avatar") { SendAvatar(ctx, pts[2]); return; } // API if (path == "/api/server") { SendJson(resp, 200, BuildServerInfo()); return; } if (path == "/api/tabs") { SendJson(resp, 200, BuildTabs()); return; } if (path == "/api/all") { SendJson(resp, 200, BuildAll()); return; } if (pts.Length >= 4 && pts[1] == "api" && pts[2] == "top") { string tabId = pts[3]; bool fame = pts.Length >= 5 && pts[4] == "fame"; var data = BuildLeaderboard(tabId, fame); if (data == null) { SendJson(resp, 404, new { error = "Tab not found" }); return; } SendJson(resp, 200, data); return; } if (pts.Length == 4 && pts[1] == "api" && pts[2] == "player") { var data = BuildPlayerProfile(pts[3]); if (data == null) { SendJson(resp, 404, new { error = "Player not found" }); return; } SendJson(resp, 200, data); return; } SendJson(resp, 404, new { error = "Not found" }); } catch (Exception ex) { try { SendJson(resp, 500, new { error = ex.Message }); } catch { } } } private void SendJson(HttpListenerResponse resp, int code, object data) { resp.StatusCode = code; resp.ContentType = "application/json; charset=utf-8"; byte[] buf = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data, Formatting.Indented)); resp.ContentLength64 = buf.Length; resp.OutputStream.Write(buf, 0, buf.Length); resp.OutputStream.Close(); } private void SendHtml(HttpListenerResponse resp, string html) { resp.StatusCode = 200; resp.ContentType = "text/html; charset=utf-8"; byte[] buf = Encoding.UTF8.GetBytes(html); resp.ContentLength64 = buf.Length; resp.OutputStream.Write(buf, 0, buf.Length); resp.OutputStream.Close(); } // ── JSON Export ────────────────────────────────────────────── private void ExportJson() { if (MajestatTopCore == null) return; try { string json = JsonConvert.SerializeObject(BuildAll(), Formatting.Indented); string path = Path.Combine(Interface.Oxide.DataDirectory, "MajestatTopWeb.json"); File.WriteAllText(path, json, Encoding.UTF8); } catch (Exception ex) { PrintError("JSON export error: " + ex.Message); } } // ── Data Builders ──────────────────────────────────────────── private object BuildServerInfo() { var info = MajestatTopCore?.Call("API_GetServerInfo") as Dictionary; if (info == null) return new { error = "Core unavailable" }; int upSec = info.ContainsKey("uptime") ? Convert.ToInt32(info["uptime"]) : 0; info["uptimeFormatted"] = (upSec / 3600) + "h " + ((upSec % 3600) / 60) + "m"; info["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); info["timestampUtc"] = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); return info; } private object BuildTabs() => MajestatTopCore?.Call("API_GetTabs"); private object BuildLeaderboard(string tabId, bool fame) { var entries = MajestatTopCore?.Call("API_GetLeaderboard", tabId, fame, _cfg.LeaderboardRows) as List>; if (entries == null) return null; return new { tab = tabId, fame, count = entries.Count, updated = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), entries }; } private object BuildPlayerProfile(string steamId) { var stats = MajestatTopCore?.Call("API_GetAllStats", steamId, false) as Dictionary; var fame = MajestatTopCore?.Call("API_GetAllStats", steamId, true) as Dictionary; if (stats == null) return null; var pl = BasePlayer.FindByID(ulong.Parse(steamId)); return new { steamId, name = pl?.displayName ?? steamId, online = pl != null, stats, fameStats = fame, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; } private object BuildAll() { var tabs = MajestatTopCore?.Call("API_GetTabs") as List>; var lb = new Dictionary(); var fm = new Dictionary(); if (tabs != null) foreach (var t in tabs) { string id = t["id"].ToString(); lb[id] = BuildLeaderboard(id, false); fm[id] = BuildLeaderboard(id, true); } return new { server = BuildServerInfo(), tabs, leaderboards = lb, fame = fm, timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }; } // ── CSS Shared ─────────────────────────────────────────────── private string SharedCss() { return ":root{--bg:#0d0d0d;--bg2:#161616;--bg3:#1e1e1e;--border:#2a2a2a;--accent:#c8572a;--accent2:#e07840;--text:#d4c9b8;--text2:#8a8070;--green:#4a9e5c;--gold:#c8a84b;--blue:#4a7ec8;}\n" + "*{box-sizing:border-box;margin:0;padding:0;}\n" + "body{background:var(--bg);color:var(--text);font-family:'Segoe UI',sans-serif;font-size:14px;}\n" + "a{color:var(--accent2);text-decoration:none;}a:hover{text-decoration:underline;}\n" + "header{background:var(--bg2);border-bottom:2px solid var(--accent);padding:14px 24px;display:flex;align-items:center;gap:16px;justify-content:space-between;flex-wrap:nowrap;overflow:hidden;position:relative;}\n" + "header h1{font-size:18px;color:var(--accent2);white-space:nowrap;flex-shrink:0;}\n" + ".back{color:var(--text2);font-size:13px;}\n" + "main{padding:24px;max-width:1200px;margin:0 auto;}\n" + /* tabs */ ".tabs-bar{background:var(--bg2);border-bottom:1px solid var(--border);padding:0 24px;display:flex;gap:4px;align-items:stretch;}\n" + ".tab-btn{background:transparent;border:none;border-bottom:3px solid transparent;color:var(--text2);padding:12px 20px;cursor:pointer;font-size:13px;font-weight:bold;letter-spacing:1px;transition:color .2s,border-color .2s;}\n" + ".tab-btn:hover{color:var(--text);border-color:var(--border);}\n" + ".tab-btn.active{color:var(--accent2);border-color:var(--accent);}\n" + ".tab-content{display:none;}.tab-content.active{display:block;}\n" + ".tabs-bar-right{margin-left:auto;display:flex;align-items:center;padding:0 4px;gap:6px;font-size:12px;color:var(--text2);white-space:nowrap;}\n" + ".tabs-bar-right .ub-val{font-size:15px;font-weight:bold;color:var(--accent2);margin-left:4px;}\n" + /* section */ ".section{background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:24px;overflow:hidden;}\n" + ".section-title{background:var(--bg3);border-bottom:1px solid var(--border);padding:12px 20px;font-size:12px;font-weight:bold;color:var(--text2);text-transform:uppercase;letter-spacing:1px;}\n" + ".section-title.fame{color:var(--gold);}\n" + /* table */ ".table-wrap{overflow-x:auto;-webkit-overflow-scrolling:touch;}\n" + "table{width:100%;border-collapse:collapse;}\n" + "th{background:var(--bg3);color:var(--text2);font-size:11px;text-transform:uppercase;letter-spacing:1px;padding:10px 14px;text-align:center;border-bottom:1px solid var(--border);white-space:normal;line-height:1.3;}\n" + "th:first-child{text-align:center;}th:nth-child(2){text-align:left;}\n" + "th.xp{color:var(--gold);}\n" + "td{padding:9px 14px;border-bottom:1px solid var(--border);color:var(--text);text-align:center;}\n" + "td:first-child{text-align:center;}td:nth-child(2){text-align:left;}\n" + "tr:last-child td{border-bottom:none;}\n" + "tr:hover td{background:rgba(255,255,255,0.03);}\n" + ".rank{color:var(--text2);font-size:12px;}\n" + ".rank-1{color:#f0c040;font-weight:bold;}.rank-2{color:#b0b0b0;font-weight:bold;}.rank-3{color:#c07040;font-weight:bold;}\n" + ".player-name{font-weight:500;}\n" + ".val-xp{color:var(--gold);font-weight:bold;}.val-total{color:var(--accent2);font-weight:bold;}\n" + ".val-time{color:var(--blue);}.val-num{color:var(--text);}\n" + ".no-data{padding:24px;text-align:center;color:var(--text2);}\n" + /* header stats */ ".server-stats{display:flex;gap:16px;flex-wrap:wrap;}\n" + ".stat-badge{background:var(--bg3);border:1px solid var(--border);border-radius:6px;padding:6px 14px;text-align:center;}\n" + ".stat-badge .val{font-size:20px;font-weight:bold;color:var(--accent2);}\n" + ".stat-badge .lbl{font-size:11px;color:var(--text2);text-transform:uppercase;letter-spacing:1px;}\n" + ".info-bar{background:var(--bg3);border-bottom:1px solid var(--border);padding:8px 24px;display:flex;gap:24px;flex-wrap:wrap;font-size:12px;color:var(--text2);}\n" + ".info-bar span{color:var(--text);}\n" + /* footer */ "footer{text-align:center;padding:14px 16px;font-size:11px;color:var(--text2);border-top:1px solid var(--border);margin-top:8px;line-height:1.8;}\n" + "footer .footer-copy{font-size:10px;color:var(--text2);opacity:0.5;margin-top:2px;}\n" + /* responsive */ "@media(max-width:768px){.server-stats{gap:8px;}.stat-badge .val{font-size:15px;}th,td{padding:7px 8px;font-size:12px;}}\n"; } // ── Footer ─────────────────────────────────────────────────── private string BuildFooter() { string coreVer = MajestatTopCore?.Version.ToString() ?? "?"; // Czas pobierany z przeglądarki klienta przez JS return "
" + "MajestatTopWeb v" + PluginVersion + " • MajestatTopCore v" + coreVer + " • " + "
created by Wo0t © Koldrix Group 2007 –
" + "
" + ""; } // ── Main Page ──────────────────────────────────────────────── private string BuildMainPage() { var serverRaw = MajestatTopCore?.Call("API_GetServerInfo") as Dictionary; var tabsRaw = MajestatTopCore?.Call("API_GetTabs") as List>; string srvName = Get(serverRaw, "name", "Rust Server"); int players = GetInt(serverRaw, "players"); int maxPl = GetInt(serverRaw, "maxPlayers"); string map = Get(serverRaw, "map", "Unknown"); int seed = GetInt(serverRaw, "seed"); int wSize = GetInt(serverRaw, "worldSize"); int upSec = GetInt(serverRaw, "uptime"); string desc = Get(serverRaw, "description", ""); int uniquePl = GetInt(serverRaw, "uniquePlayers"); string uptime = (upSec / 3600) + "h " + ((upSec % 3600) / 60) + "m"; string mapUrl = "https://rustmaps.com/map/" + wSize + "/" + seed; var tabBtns = new StringBuilder(); var tabCont = new StringBuilder(); if (tabsRaw != null) { for (int i = 0; i < tabsRaw.Count; i++) { string id = tabsRaw[i]["id"].ToString(); string label = tabsRaw[i]["label"].ToString(); bool first = i == 0; tabBtns.Append(""); tabCont.Append("
"); var entries = MajestatTopCore?.Call("API_GetLeaderboard", id, false, _cfg.LeaderboardRows) as List>; tabCont.Append(BuildLeaderboardHtml(label, entries, false)); var fameEnt = MajestatTopCore?.Call("API_GetLeaderboard", id, true, _cfg.LeaderboardRows) as List>; tabCont.Append(BuildLeaderboardHtml(label + " — HALL OF FAME", fameEnt, true)); tabCont.Append("
"); } } var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("" + EscHtml(srvName) + " — Stats"); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("
"); sb.AppendLine("

" + EscHtml(srvName) + " STATS

"); sb.AppendLine("
"); sb.AppendLine("
" + players + "/" + maxPl + "
Online
"); sb.AppendLine("
" + uptime + "
Uptime
"); sb.AppendLine("
" + EscHtml(map) + "
Mapa
"); sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("
Seed: " + seed + "
"); sb.AppendLine("
Rozmiar: " + wSize + "
"); sb.AppendLine(" "); string srvType = GetServerType(); string wipeSch = GetWipeSchedule(); if (!string.IsNullOrEmpty(srvType)) sb.AppendLine("
Typ: " + EscHtml(srvType) + "
"); if (!string.IsNullOrEmpty(wipeSch)) sb.AppendLine("
Wipe: " + EscHtml(wipeSch) + "
"); if (!string.IsNullOrEmpty(desc)) { string descClean = EscHtml(desc).Replace("\\n", " ").Replace("\n", " ").Trim(); sb.AppendLine("
Opis: " + descClean + "
"); } sb.AppendLine("
Odswiezanie co " + _cfg.JsonExportInterval + "s
"); sb.AppendLine("
"); sb.AppendLine("
" + tabBtns + "
👥 Unikalni gracze:" + uniquePl + "
" + "
"); sb.AppendLine("
" + tabCont + "
"); sb.AppendLine(BuildFooter()); sb.AppendLine(""); sb.AppendLine(""); return sb.ToString(); } private string BuildLeaderboardHtml(string title, List> entries, bool fame) { var sb = new StringBuilder(); sb.Append("
" + title + "
"); if (entries == null || entries.Count == 0) { sb.Append("
Brak danych
"); return sb.ToString(); } var cols = entries[0].Keys.Where(k => k != "steamId" && k != "name" && k != "rank").ToList(); sb.Append("
"); sb.Append(""); foreach (var col in cols) { string lbl = FormatColumnLabel(col); string cls = col == "XP.Total" ? " class=\"xp\"" : ""; sb.Append("" + lbl + ""); } sb.Append(""); foreach (var e in entries) { int rank = Convert.ToInt32(e["rank"]); string rankCls = rank == 1 ? "rank-1" : rank == 2 ? "rank-2" : rank == 3 ? "rank-3" : "rank"; string name = EscHtml(e["name"].ToString()); string sid = e["steamId"].ToString(); sb.Append(""); sb.Append(""); foreach (var col in cols) { double val = e.ContainsKey(col) ? Convert.ToDouble(e[col]) : 0; string cls = col == "XP.Total" ? "val-xp" : col.EndsWith(".Total") ? "val-total" : col == "PlayTime.Seconds" ? "val-time" : "val-num"; string disp = col == "PlayTime.Seconds" ? FormatTime((long)val) : FormatNum(val); sb.Append(""); } sb.Append(""); } sb.Append("
#Gracz
" + rank + "" + name + "" + disp + "
"); return sb.ToString(); } // ── Player Profile Page ────────────────────────────────────── private string BuildPlayerPage(string steamId) { var stats = MajestatTopCore?.Call("API_GetAllStats", steamId, false) as Dictionary; var fame = MajestatTopCore?.Call("API_GetAllStats", steamId, true) as Dictionary; if (stats == null) return null; var serverRaw = MajestatTopCore?.Call("API_GetServerInfo") as Dictionary; string srvName = Get(serverRaw, "name", "Rust Server"); int players = GetInt(serverRaw, "players"); int maxPl = GetInt(serverRaw, "maxPlayers"); string map = Get(serverRaw, "map", "Unknown"); int upSec = GetInt(serverRaw, "uptime"); string uptime = (upSec / 3600) + "h " + ((upSec % 3600) / 60) + "m"; var player = BasePlayer.FindByID(ulong.Parse(steamId)); bool online = player != null; // Pobierz nick z cache Core (działa też dla offline graczy) string name = steamId; if (player != null) name = player.displayName; else { // Pobierz z API_GetAllStats — profil zawiera Name var profileData = MajestatTopCore?.Call("API_GetPlayerName", steamId) as string; if (!string.IsNullOrEmpty(profileData)) name = profileData; else if (stats != null) { // Fallback — szukaj w leaderboardzie var tabs = MajestatTopCore?.Call("API_GetTabs") as List>; if (tabs != null) { foreach (var t in tabs) { var lb = MajestatTopCore?.Call("API_GetLeaderboard", t["id"].ToString(), false, 1000) as List>; if (lb == null) continue; var entry = lb.FirstOrDefault(e => e.ContainsKey("steamId") && e["steamId"].ToString() == steamId); if (entry != null && entry.ContainsKey("name")) { name = entry["name"].ToString(); break; } } } } } string avatarUrl = "/avatar/" + steamId; string steamUrl = "https://steamcommunity.com/profiles/" + steamId; // Stale grupy — zawsze wyswietlaj wszystkie klucze nawet gdy 0 string[] pvpKeys = { "PvP.Kills","PvP.Deaths","PvE.Deaths", "Kill.Heli","Kill.Bradley","Kill.Scientist","Kill.Zombie","Kill.Animal","PlayTime.Seconds" }; string[] pvpKeysFame = { "XP.Total","PvP.Kills","PvP.Deaths","PvE.Deaths", "Kill.Heli","Kill.Bradley","Kill.Scientist","Kill.Zombie","Kill.Animal","PlayTime.Seconds" }; string[] gatherKeys = { "Gather.Total","Gather.Wood","Gather.Stone", "Gather.Metal","Gather.Sulfur","Gather.Scrap","Gather.Food" }; string[] lootKeys = { "Loot.Total","Loot.EliteCrate","Loot.MilitaryCrate", "Loot.LockedCrate","Loot.HackableCrate","Loot.Airdrop", "Loot.UnderwaterCrate","Loot.NormalCrate","Loot.Barrel" }; string[] buildKeys = { "Build.Total","Build.Walls","Build.Floors","Build.Doors", "Build.Turrets","Build.Traps","Build.Electrical","Build.Pipes" }; string profileCss = ".profile-header{display:flex;align-items:flex-end;gap:20px;padding:20px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:24px;flex-wrap:wrap;}\n" + ".profile-left{display:flex;flex-direction:column;align-items:center;gap:8px;flex-shrink:0;}\n" + ".avatar-link{display:block;text-decoration:none;}\n" + ".avatar-link:hover .avatar-wrap img,.avatar-link:hover .avatar-placeholder{border-color:var(--accent2);opacity:0.85;}\n" + ".avatar-wrap{position:relative;width:96px;height:96px;}\n" + ".avatar-wrap img{width:96px;height:96px;border-radius:8px;border:2px solid var(--accent);object-fit:cover;background:var(--bg3);display:block;transition:border-color .2s,opacity .2s;}\n" + ".avatar-placeholder{width:96px;height:96px;border-radius:8px;border:2px solid var(--accent);background:var(--bg3);display:flex;align-items:center;justify-content:center;font-size:36px;color:var(--text2);transition:border-color .2s,opacity .2s;}\n" + ".online-badge{font-size:12px;font-weight:bold;padding:3px 10px;border-radius:4px;text-align:center;white-space:nowrap;}\n" + ".online-badge.online{background:#1a3a1a;color:#4a9e5c;border:1px solid #4a9e5c;}\n" + ".online-badge.offline{background:var(--bg3);color:var(--text2);border:1px solid var(--border);}\n" + ".profile-info{flex:1;min-width:180px;display:flex;flex-direction:column;justify-content:space-between;align-self:stretch;}\n" + ".profile-name-link{text-decoration:none;}\n" + ".profile-name-link:hover h2{color:var(--text);text-decoration:underline;}\n" + ".profile-info h2{font-size:22px;color:var(--accent2);margin-bottom:6px;transition:color .2s;}\n" + ".profile-info .sub{font-size:12px;color:var(--text2);margin-top:3px;}\n" + ".profile-xp-big{font-size:34px;font-weight:bold;color:var(--gold);line-height:1;letter-spacing:-1px;padding-bottom:2px;}\n" + ".profile-xp-big .xp-lbl{font-size:13px;color:var(--text2);font-weight:normal;margin-right:6px;vertical-align:middle;letter-spacing:0;}\n" + ".profile-links{display:flex;flex-direction:column;gap:8px;align-self:flex-end;}\n" + ".btn-steam{display:block;background:#1b2838;border:1px solid #4a90d9;color:#4a90d9;padding:8px 18px;border-radius:4px;font-size:12px;text-align:center;}\n" + ".btn-steam:hover{background:#2a3f5f;}\n" + ".section-label{font-size:11px;font-weight:bold;color:var(--text2);text-transform:uppercase;letter-spacing:2px;margin-bottom:12px;padding-left:2px;}\n" + ".section-label.fame{color:var(--gold);}\n" + ".stats-row-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:28px;}\n" + "@media(max-width:900px){.stats-row-grid{grid-template-columns:repeat(2,1fr);}}\n" + "@media(max-width:500px){.stats-row-grid{grid-template-columns:1fr;}}\n" + ".stats-card{background:var(--bg2);border:1px solid var(--border);border-radius:8px;overflow:hidden;}\n" + ".stats-card.fame{border-color:#c8a84b33;}\n" + ".stats-card-title{background:var(--bg3);border-bottom:1px solid var(--border);padding:10px 16px;font-size:11px;font-weight:bold;color:var(--text2);text-transform:uppercase;letter-spacing:1px;}\n" + ".stats-card.fame .stats-card-title{color:var(--gold);}\n" + ".stats-row{display:flex;justify-content:space-between;align-items:center;padding:7px 16px;border-bottom:1px solid var(--border);font-size:13px;}\n" + ".stats-row:last-child{border-bottom:none;}\n" + ".stats-row .key{color:var(--text2);}\n" + ".v-xp{color:#c8a84b;font-weight:bold;}.v-total{color:#e07840;font-weight:bold;}.v-time{color:#4a7ec8;}.v-num{color:var(--text);}\n"; var sb = new StringBuilder(); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine(""); sb.AppendLine("" + EscHtml(name) + " — " + EscHtml(srvName) + ""); sb.AppendLine(""); sb.AppendLine(""); // Header profilu: nazwa serwera (lewo) | PROFIL GRACZA (środek) | statsy (prawo) sb.AppendLine("
"); sb.AppendLine("

" + EscHtml(srvName) + " STATS

"); sb.AppendLine("
Profil Gracza
"); sb.AppendLine("
"); sb.AppendLine("
" + players + "/" + maxPl + "
Online
"); sb.AppendLine("
" + uptime + "
Uptime
"); sb.AppendLine("
" + EscHtml(map) + "
Mapa
"); sb.AppendLine("
"); sb.AppendLine("
"); // Info-bar z przyciskiem powrót po lewej sb.AppendLine("
"); sb.AppendLine(" ← Powrot do rankingu"); sb.AppendLine("
"); sb.AppendLine("
"); // XP gracza double xpTotal = stats != null && stats.ContainsKey("XP.Total") ? stats["XP.Total"] : 0; string xpFormatted = FormatXp(xpTotal); // Profile header sb.AppendLine("
"); // Lewa: avatar klikalny → Steam, badge pod spodem sb.AppendLine(" "); // Środek: nick klikalny → Steam na górze, XP na dole sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("

" + EscHtml(name) + "

"); sb.AppendLine("
SteamID64: " + steamId + "
"); sb.AppendLine("
Nick z ostatniej sesji na serwerze
"); sb.AppendLine("
"); sb.AppendLine("
XP" + xpFormatted + "
"); sb.AppendLine("
"); sb.AppendLine("
"); // Wiersz 1 — sezonowe sb.AppendLine("
Statystyki sezonowe
"); sb.AppendLine("
"); sb.Append(BuildStatsCard("PVP / PVE", pvpKeys, stats, false)); sb.Append(BuildStatsCard("GATHER", gatherKeys, stats, false)); sb.Append(BuildStatsCard("LOOT", lootKeys, stats, false)); sb.Append(BuildStatsCard("BUILD", buildKeys, stats, false)); sb.AppendLine("
"); // Wiersz 2 — Fame sb.AppendLine("
Hall of Fame
"); sb.AppendLine("
"); sb.Append(BuildStatsCard("FAME — PVP", pvpKeysFame, fame, true)); sb.Append(BuildStatsCard("FAME — GATHER", gatherKeys, fame, true)); sb.Append(BuildStatsCard("FAME — LOOT", lootKeys, fame, true)); sb.Append(BuildStatsCard("FAME — BUILD", buildKeys, fame, true)); sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine(BuildFooter()); sb.AppendLine(""); return sb.ToString(); } private string BuildStatsCard(string title, string[] keys, Dictionary stats, bool isFame) { var sb = new StringBuilder(); sb.Append("
"); sb.Append("
" + title + "
"); foreach (var key in keys) { double val = stats != null && stats.ContainsKey(key) ? stats[key] : 0; string label = key.Contains('.') ? key.Substring(key.IndexOf('.') + 1) : key; string cls, disp; if (key == "PlayTime.Seconds") { cls = "v-time"; disp = FormatTime((long)val); } else if (key == "XP.Total") { cls = "v-xp"; disp = FormatNum(val); } else if (key.EndsWith(".Total")) { cls = "v-total"; disp = FormatNum(val); } else { cls = "v-num"; disp = FormatNum(val); } sb.Append("
"); sb.Append("" + EscHtml(label) + ""); sb.Append("" + disp + ""); sb.Append("
"); } sb.Append("
\n"); return sb.ToString(); } // Cache URL avatarów steamId → url private Dictionary _avatarCache = new Dictionary(); // Pobierz avatar gracza z Steam i odeślij jako redirect private void SendAvatar(HttpListenerContext ctx, string steamId) { // Sprawdź cache if (_avatarCache.TryGetValue(steamId, out string cachedUrl)) { ctx.Response.StatusCode = 302; ctx.Response.AddHeader("Location", cachedUrl); ctx.Response.Close(); return; } // Próbuj pobrać URL avatara ze Steam XML API string fallback = "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg"; try { string xmlUrl = "https://steamcommunity.com/profiles/" + steamId + "?xml=1"; var wc = new System.Net.WebClient(); wc.Headers["User-Agent"] = "MajestatTopWeb/1.0"; string xml = wc.DownloadString(xmlUrl); int start = xml.IndexOf("= 0) { start += "", start); if (end > start) { string avatarUrl = xml.Substring(start, end - start).Trim(); if (!string.IsNullOrEmpty(avatarUrl)) { _avatarCache[steamId] = avatarUrl; ctx.Response.StatusCode = 302; ctx.Response.AddHeader("Location", avatarUrl); ctx.Response.Close(); return; } } } } catch { } _avatarCache[steamId] = fallback; ctx.Response.StatusCode = 302; ctx.Response.AddHeader("Location", fallback); ctx.Response.Close(); } // ── Server Tags (from server.tags ConVar) ──────────────────── private string GetServerType() { try { string tags = ConVar.Server.tags ?? ""; var parts = tags.Split(','); foreach (var tag in parts) { string t = tag.Trim().ToLower(); if (t == "pve") return "PvE"; if (t == "softcore") return "Softcore"; } foreach (var tag in parts) { string t = tag.Trim().ToLower(); if (t == "modded") return "Modded"; if (t == "vanilla") return "Vanilla"; } return !string.IsNullOrEmpty(_cfg.ServerType) ? _cfg.ServerType : ""; } catch { return !string.IsNullOrEmpty(_cfg.ServerType) ? _cfg.ServerType : ""; } } private string GetWipeSchedule() { try { string tags = ConVar.Server.tags ?? ""; var parts = tags.Split(','); foreach (var tag in parts) { string t = tag.Trim().ToLower(); switch (t) { case "weekly": return "Weekly"; case "biweekly": return "Biweekly"; case "monthly": return "Monthly"; case "daily": return "Daily"; } } return !string.IsNullOrEmpty(_cfg.WipeSchedule) ? _cfg.WipeSchedule : ""; } catch { return !string.IsNullOrEmpty(_cfg.WipeSchedule) ? _cfg.WipeSchedule : ""; } } // ── Helpers ────────────────────────────────────────────────── private string Get(Dictionary d, string key, string def) => d != null && d.ContainsKey(key) ? d[key]?.ToString() ?? def : def; private int GetInt(Dictionary d, string key) => d != null && d.ContainsKey(key) ? Convert.ToInt32(d[key]) : 0; // Podziel długą nazwę kolumny na dwie linie HTML // np. "UnderwaterCrate" → "UNDERWATER
CRATE" private string FormatColumnLabel(string col) { if (col == "PlayTime.Seconds") return "CZAS
GRY"; // Wyciągnij część po kropce string raw = col.Contains(".") ? col.Substring(col.IndexOf('.') + 1) : col; // Mapa czytelnych nazw switch (raw) { case "Total": return col == "XP.Total" ? "TOTAL
XP" : "TOTAL"; case "Kills": return "KILLS"; case "Deaths": return "DEATHS"; case "Heli": return "HELI"; case "Bradley": return "BRADLEY"; case "Scientist": return "SCIENTIST"; case "Zombie": return "ZOMBIE"; case "Animal": return "ANIMAL"; case "Wood": return "WOOD"; case "Stone": return "STONE"; case "Metal": return "METAL"; case "Sulfur": return "SULFUR"; case "Scrap": return "SCRAP"; case "Food": return "FOOD"; case "EliteCrate": return "ELITE
CRATE"; case "MilitaryCrate": return "MILITARY
CRATE"; case "LockedCrate": return "LOCKED
CRATE"; case "HackableCrate": return "HACKABLE
CRATE"; case "Airdrop": return "AIRDROP"; case "UnderwaterCrate": return "UNDERWATER
CRATE"; case "NormalCrate": return "NORMAL
CRATE"; case "Barrel": return "BARREL"; case "Walls": return "WALLS"; case "Floors": return "FLOORS"; case "Doors": return "DOORS"; case "Turrets": return "TURRETS"; case "Traps": return "TRAPS"; case "Electrical": return "ELECTRICAL"; case "Pipes": return "PIPES"; default: return raw.ToUpper(); } } private string EscHtml(string s) { if (string.IsNullOrEmpty(s)) return ""; return s.Replace("&", "&").Replace("<", "<").Replace(">", ">") .Replace("\"", """).Replace("'", "'"); } private string FormatTime(long seconds) { if (seconds <= 0) return "—"; long h = seconds / 3600; long m = (seconds % 3600) / 60; return h > 0 ? h + "h " + m.ToString("D2") + "m" : m + "m"; } private string FormatNum(double val) { string fmt = (_cfg.TableNumberFormat ?? "KM").ToUpper(); int dec = _cfg.TableNumberDecimals; string decFmt = "F" + dec; if (fmt.Contains("M") && val >= 1_000_000) return (val / 1_000_000).ToString(decFmt) + "M"; if (fmt.Contains("K") && val >= 1_000) return (val / 1_000).ToString(decFmt) + "K"; return ((long)val).ToString(); } private string FormatXp(double val) { string fmt = (_cfg.ProfileXpFormat ?? "KM").ToUpper(); int dec = _cfg.ProfileXpDecimals; string decFmt = "F" + dec; if (fmt.Contains("M") && val >= 1_000_000) return (val / 1_000_000).ToString(decFmt) + "M"; if (fmt.Contains("K") && val >= 1_000) return (val / 1_000).ToString(decFmt) + "K"; return ((long)val).ToString(); } // ── Update Checker ─────────────────────────────────────────── private void StartUpdateChecker() { try { var iv = MajestatTopCore?.Call("API_GetUpdateInterval"); _updateIntervalMinutes = iv != null ? Convert.ToInt32(iv) : 60; } catch { _updateIntervalMinutes = 60; } timer.Once(5f, CheckForUpdate); if (_updateIntervalMinutes > 0) timer.Every(_updateIntervalMinutes * 60f, CheckForUpdate); } void OnMajestatUpdateCheck() { webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void CheckForUpdate() { webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void OnUpdateResponse(int code, string response) { string pluginName = "MajestatTopWeb"; if (code == 0 || code >= 400 || string.IsNullOrEmpty(response)) { Puts("[Update] Nie mozna sprawdzic aktualizacji (HTTP " + code + ")."); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); return; } try { var data = JsonConvert.DeserializeObject>(response); string latest = data != null && data.ContainsKey("version") ? data["version"].Trim() : null; if (latest != null && IsNewerVersion(latest, PluginVersion)) { string url = data.ContainsKey("url") ? data["url"] : PluginUpdateUrl; PrintWarning("[Update] Dostepna nowsza wersja (" + latest + ") - Twoja (" + PluginVersion + ")"); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, latest, url); } else { Puts("[Update] Wersja aktualna (" + PluginVersion + ")."); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); } } catch (Exception ex) { Puts("[Update] Blad: " + ex.Message); MajestatTopCore?.Call("API_ReportUpdateResult", pluginName, null, null); } } private bool IsNewerVersion(string latest, string current) { try { var l = Array.ConvertAll(latest.Split('.'), int.Parse); var c = Array.ConvertAll(current.Split('.'), int.Parse); int len = Math.Max(l.Length, c.Length); for (int i = 0; i < len; i++) { int lv = i < l.Length ? l[i] : 0; int cv = i < c.Length ? c[i] : 0; if (lv > cv) return true; if (lv < cv) return false; } return false; } catch { return false; } } } }