using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Text; using System.Threading; using Newtonsoft.Json; using Oxide.Core.Plugins; /* * MajestatTopMap 1.0.0 * ==================== * Moduł mapy dla systemu MajestatTop. * Wymaga: MajestatTopCore 3.2.0+, MajestatTopWeb 1.2.0+ * * Serwuje endpointy pod tym samym portem co MajestatTopWeb: * GET /api/map/data — pozycje encji (filtrowane wg sesji) * GET /api/map/monuments — lista monumentów * GET /api/map/info — rozmiar/seed mapy * GET /api/map/image — obraz mapy PNG jako base64 JSON * GET /api/map/stream — SSE stream pozycji co 2s * * Zakładka Mapa jest wstrzykiwana do MajestatTopWeb przez hook OnMajestatMapReady. */ namespace Oxide.Plugins { [Info("MajestatTopMap", "Wo0t", "1.0.0")] [Description("Interactive map module for MajestatTop — player positions, monuments, events, mini-chat.")] class MajestatTopMap : RustPlugin { [PluginReference] Plugin MajestatTopCore; private void MLog(int level, string msg) { int eff = MajestatTopCore != null ? (int)(MajestatTopCore.Call("API_GetLogLevel") ?? 1) : 1; if (level < eff) return; 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); } [PluginReference] Plugin MajestatTopWeb; private const string PluginVersion = "1.0.0"; private static readonly string PluginUpdateUrl = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String("aHR0cDovL2Rvd25sb2FkLmtvbGRyaXguY29tL3J1c3QvcGx1Z2lucy9NYWplc3RhdFRvcE1hcC92ZXJzaW9uLmpzb24=")); private int _updateIntervalMinutes = 60; // ── Config ─────────────────────────────────────────────────── private class ConfigData { [JsonProperty("Map update interval (seconds)")] public float UpdateInterval = 2f; [JsonProperty("Map image refresh interval (seconds, 0=never)")] public float ImageRefreshInterval = 0f; [JsonProperty("Show sleeping players for admin")] public bool ShowSleepers = true; [JsonProperty("Mini-chat height (px)")] public int ChatHeight = 220; [JsonProperty("Map file path override (leave empty for auto-detect)")] public string MapFilePath = ""; [JsonProperty("Map sea margin (units added by rendermap on each side, default 500)")] public int MapSeaMargin = 500; } private ConfigData _cfg; protected override void LoadDefaultConfig() => _cfg = new ConfigData(); protected override void LoadConfig() { base.LoadConfig(); try { _cfg = Config.ReadObject() ?? new ConfigData(); } catch { _cfg = new ConfigData(); } if (_cfg.ImageRefreshInterval == 300f) _cfg.ImageRefreshInterval = 0f; SaveConfig(); } protected override void SaveConfig() => Config.WriteObject(_cfg); // ── State ──────────────────────────────────────────────────── private Timer _imageTimer; private List _mapSseClients = new List(); private readonly object _sseLock = new object(); private bool _registered = false; // ── Lifecycle ──────────────────────────────────────────────── void OnServerInitialized() { if (MajestatTopCore == null) { PrintError("MajestatTopCore is not loaded!"); return; } if (!CheckMinVersion(MajestatTopCore.Version, 3, 2, 0)) { PrintError($"MajestatTopMap wymaga MajestatTopCore 3.2.0+. Zainstalowana: {MajestatTopCore.Version}"); return; } if (MajestatTopWeb == null) { PrintWarning("MajestatTopWeb is not loaded - map will be unavailable in Web."); } else if (!CheckMinVersion(MajestatTopWeb.Version, 1, 2, 0)) { PrintError($"MajestatTopMap wymaga MajestatTopWeb 1.2.0+. Zainstalowana: {MajestatTopWeb.Version}"); return; } // Zarejestruj w Core module registry MajestatTopCore.Call("API_RegisterModule", new Dictionary { ["name"] = "MajestatTopMap", ["version"] = PluginVersion, ["author"] = "Wo0t", ["description"] = "Interactive map with player positions & mini-chat" }); // Wygeneruj obraz mapy GenerateMapImage(); if (_cfg.ImageRefreshInterval > 0) _imageTimer = timer.Every(_cfg.ImageRefreshInterval, GenerateMapImage); // Zarejestruj się w Web RegisterInWeb(); // Updater StartUpdateChecker(); } private bool CheckMinVersion(Oxide.Core.VersionNumber v, int major, int minor, int patch) => v.Major > major || (v.Major == major && (v.Minor > minor || (v.Minor == minor && v.Patch >= patch))); // ── 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() => CheckForUpdate(); private void CheckForUpdate() { webrequest.Enqueue(PluginUpdateUrl, null, OnUpdateResponse, this); } private void OnUpdateResponse(int code, string response) { if (code == 0 || code >= 400 || string.IsNullOrEmpty(response)) { MajestatTopCore?.Call("API_ReportUpdateResult", "MajestatTopMap", 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; MLog(2, "[Update] Newer version available (" + latest + ") - your version (" + PluginVersion + ")"); MajestatTopCore?.Call("API_ReportUpdateResult", "MajestatTopMap", latest, url); } else { MLog(1, "[Update] Up to date (" + PluginVersion + ") - no updates."); MajestatTopCore?.Call("API_ReportUpdateResult", "MajestatTopMap", null, null); } } catch { MajestatTopCore?.Call("API_ReportUpdateResult", "MajestatTopMap", null, null); } } private bool IsNewerVersion(string latest, string current) { try { string lc = latest != null && latest.Contains("-") ? latest.Substring(0, latest.IndexOf('-')) : latest; string cc = current != null && current.Contains("-") ? current.Substring(0, current.IndexOf('-')) : current; var l = System.Array.ConvertAll(lc.Split('.'), int.Parse); var c = System.Array.ConvertAll(cc.Split('.'), int.Parse); int len = System.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; } } void OnPluginLoaded(Plugin p) { if (p?.Name == "MajestatTopCore") OnServerInitialized(); if (p?.Name == "MajestatTopWeb" && !_registered) RegisterInWeb(); } void OnPluginUnloaded(Plugin p) { if (p?.Name == "MajestatTopCore") { _imageTimer?.Destroy(); CloseAllSse(); } } void Unload() { _imageTimer?.Destroy(); CloseAllSse(); MajestatTopWeb?.Call("API_UnregisterMapModule"); MajestatTopCore?.Call("API_UnregisterModule", "MajestatTopMap"); } private void RegisterInWeb() { if (MajestatTopWeb == null) return; var result = MajestatTopWeb.Call("API_RegisterMapModule", new Dictionary { ["version"] = PluginVersion }); _registered = result != null && (bool)result; if (_registered) MLog(1, "Zarejestrowano w MajestatTopWeb."); else PrintWarning("Registration in MajestatTopWeb failed - try again after Web loads."); } // ── Map image ──────────────────────────────────────────────── private string _mapPageUrl = ""; private string _mapImageBase64 = ""; private string _mapImagePath = ""; private string _expectedMapFilename = ""; private void GenerateMapImage() { int seed = (int)World.Seed; int size = (int)World.Size; _mapPageUrl = $"https://rustmaps.com/map/{size}_{seed}"; _expectedMapFilename = $"map_{size}_{seed}.png"; if (!string.IsNullOrEmpty(_cfg.MapFilePath)) { if (System.IO.File.Exists(_cfg.MapFilePath)) { MLog(1, $"[Map] Using path from cfg: {_cfg.MapFilePath}"); LoadMapFile(_cfg.MapFilePath); return; } PrintWarning($"[Map] File from cfg does not exist: {_cfg.MapFilePath}"); } string existing = FindMapFile(_expectedMapFilename); if (existing != null) { MLog(1, $"[Map] Found existing file: {existing}"); LoadMapFile(existing); return; } MLog(1, "[Map] Uruchamianie rendermap..."); ConsoleSystem.Run(ConsoleSystem.Option.Server, "rendermap"); timer.Once(45f, () => { if (!string.IsNullOrEmpty(_mapImageBase64)) return; string found = FindMapFile(_expectedMapFilename); if (found != null) LoadMapFile(found); else PrintWarning($"[Map] Not found {_expectedMapFilename}. Set the path in cfg."); }); } void OnServerMessage(string message, string name, string color, ulong id) { if (string.IsNullOrEmpty(message)) return; const string prefix = "Saved map render to:"; if (!message.StartsWith(prefix)) return; string path = message.Substring(prefix.Length).Trim(); if (string.IsNullOrEmpty(path)) return; MLog(1, $"[Map] Rust saved the map: {path}"); timer.Once(2f, () => LoadMapFile(path)); } private string FindMapFile(string filename) { var dirs = new[] { System.IO.Directory.GetCurrentDirectory(), System.IO.Path.Combine(System.IO.Directory.GetCurrentDirectory(), ".."), ConVar.Server.rootFolder ?? "", ConVar.Server.GetServerFolder("") ?? "" }; foreach (var dir in dirs) { if (string.IsNullOrEmpty(dir)) continue; try { string full = System.IO.Path.GetFullPath(System.IO.Path.Combine(dir, filename)); if (System.IO.File.Exists(full)) return full; } catch { } } return null; } private void LoadMapFile(string path) { try { // Sprawdź czy plik istnieje i jest czytelny var info = new System.IO.FileInfo(path); if (!info.Exists) throw new Exception("Plik nie istnieje"); _mapImagePath = path; _mapImageBase64 = "loaded"; // znacznik że plik jest gotowy MLog(1, $"[Map] Obraz mapy gotowy: {path} ({info.Length / 1024} KB)."); } catch (Exception ex) { PrintWarning($"[Map] Error reading {path}: {ex.Message}"); } } // Odbieranie danych mapowych przez Oxide hook void OnMajestatMapUpdate(object[] entities) { if (entities == null || _mapSseClients.Count == 0) return; byte[] data = Encoding.UTF8.GetBytes( "data: " + JsonConvert.SerializeObject(entities) + "\n\n"); lock (_sseLock) { var dead = new List(); foreach (var c in _mapSseClients) try { c.OutputStream.Write(data, 0, data.Length); c.OutputStream.Flush(); } catch { dead.Add(c); } foreach (var d in dead) _mapSseClients.Remove(d); } } void OnMajestatChatMessage(string steamId, string nick, string text, string channel, string source, bool isAdmin, long timestamp) { // Chat SSE obsługiwany przez MajestatTopWeb — Map używa polling /api/chat/messages } private void CloseAllSse() { lock (_sseLock) { foreach (var c in _mapSseClients) try { c.Close(); } catch { } _mapSseClients.Clear(); } } // ── API dla MajestatTopWeb ──────────────────────────────────── // Web wywołuje to żeby obsłużyć request mapowy // Web wywołuje to żeby dostać ścieżkę do pliku PNG [HookMethod("API_GetMapImagePath")] public string API_GetMapImagePath() => _mapImagePath; // Web wywołuje to dla endpointów /api/map/* (zwraca JSON jako string) [HookMethod("API_HandleMapJson")] public string API_HandleMapJson(string path, string steamId, bool isAdmin) { switch (path) { case "/api/map/data": { var data = MajestatTopCore?.Call("API_GetMapData", steamId, isAdmin) as List>; return JsonConvert.SerializeObject(data ?? new List>()); } case "/api/map/info": { var info = MajestatTopCore?.Call("API_GetMapInfo") as Dictionary; if (info == null) info = new Dictionary(); info["seaMargin"] = _cfg.MapSeaMargin; return JsonConvert.SerializeObject(info); } case "/api/map/monuments": { var mons = MajestatTopCore?.Call("API_GetMonuments") as List>; return JsonConvert.SerializeObject(mons ?? new List>()); } case "/api/map/image": return JsonConvert.SerializeObject(new { has_image = !string.IsNullOrEmpty(_mapImagePath) && System.IO.File.Exists(_mapImagePath), page_url = _mapPageUrl }); case "/api/map/vending": { var vms = MajestatTopCore?.Call("API_GetVendingMachines") as List>; return JsonConvert.SerializeObject(vms ?? new List>()); } default: return null; } } private void HandleMapSse(HttpListenerResponse resp, string steamId, bool isAdmin) { try { resp.ContentType = "text/event-stream; charset=utf-8"; resp.AddHeader("Cache-Control", "no-cache"); resp.AddHeader("Access-Control-Allow-Origin", "*"); resp.SendChunked = true; lock (_sseLock) { _mapSseClients.Add(resp); } // Wyślij od razu pierwsze dane var initial = MajestatTopCore?.Call("API_GetMapData", steamId, isAdmin) as List>; if (initial != null) { byte[] init = Encoding.UTF8.GetBytes( "data: " + JsonConvert.SerializeObject(initial) + "\n\n"); resp.OutputStream.Write(init, 0, init.Length); resp.OutputStream.Flush(); } // Keepalive while (true) { Thread.Sleep(15000); byte[] hb = Encoding.UTF8.GetBytes(": keep-alive\n\n"); resp.OutputStream.Write(hb, 0, hb.Length); resp.OutputStream.Flush(); } } catch { } finally { lock (_sseLock) { _mapSseClients.Remove(resp); } try { resp.Close(); } catch { } } } // SSE dla mapy — Web przekazuje response jako object [HookMethod("API_HandleMapSse")] public void API_HandleMapSse(object respObj, string steamId, bool isAdmin) { var resp = respObj as HttpListenerResponse; if (resp == null) return; new Thread(() => HandleMapSse(resp, steamId, isAdmin)) { IsBackground = true }.Start(); } [HookMethod("API_GetMapTabHtml")] public string API_GetMapTabHtml(int chatMaxLen) { return BuildMapHtml(chatMaxLen); } // ── Map HTML ───────────────────────────────────────────────── private string BuildMapHtml(int chatMaxLen) { return "
" + "
" + " " + "
" + "
" + " " + "
" + " ● Ty" + " ● Team" + " ● Gracz" + " ▲ Heli" + " ■ Bradley" + " ♦ Kargo" + " ▲ CH47" + " ◆ Sklep" + "
" + "
" + "
Zaloguj się przez Steam aby widzieć graczy na mapie
" + "
" + "
" + "
" + "
" + " " + " " + " " + " " + " " + " " + "
" + "
" + "
" + "
" + " " + "
" + " " + "
" + "
"; } } }