using System; using System.Collections.Generic; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries; namespace Oxide.Plugins { [Info("MajestatTopNet", "Wo0t", "1.0.2")] [Description("Lekki reporter — zgłasza serwer do Majestat Network (klient: Net). Bez zależności.")] public class MajestatTopNet : RustPlugin { // Adres Mastera — zaszyty i zaciemniony (base64), jak w Core. To tylko // utrudnienie: w pliku .cs nie da się URL ukryć w 100%. Master i tak sam // decyduje o weryfikacji/premium, więc podmiana szkodzi tylko temu serwerowi. private static readonly string MasterUrl = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String( "aHR0cHM6Ly9tYXN0ZXIubWFqZXN0YXRuZXR3b3JrLmtvbGRyaXguY29tL2FwaS9oZWFydGJlYXQucGhw")); // Co ile wysyłać heartbeat (na sztywno — bez zaśmiecania configu). private const float Interval = 60f; // Build raportowany na liście (semver w [Info] musi być czysty X.Y.Z dla Carbon). private const string Build = "1.0.2-070620261140"; private ConfigData _cfg; private Timer _timer; private string _publicIp = ""; private string _serverId = ""; private string _legacyServerId = null; // ── Konfiguracja (minimalna) ───────────────────────────────── private class ConfigData { // Verification token — issued in the Master panel; gives the ✓ badge. [JsonProperty("Token")] public string Token = ""; [JsonProperty("Game Port")] public int GamePort = 0; } protected override void LoadDefaultConfig() => _cfg = new ConfigData(); protected override void LoadConfig() { base.LoadConfig(); try { _legacyServerId = Config["Server ID (auto, nie ruszaj)"] as string; } catch { _legacyServerId = null; } try { _cfg = Config.ReadObject(); if (_cfg == null) throw new Exception("pusty config"); SaveConfig(); // dopisze nowe pola, ZACHOWUJĄC istniejące } catch (Exception ex) { _cfg = new ConfigData(); // NIE nadpisujemy uszkodzonego pliku — robimy kopię try { string path = Config.Filename; if (System.IO.File.Exists(path)) { string bak = path + ".broken-" + DateTime.Now.ToString("yyyyMMdd-HHmmss"); System.IO.File.Copy(path, bak, true); PrintError($"[Config] Uszkodzony ({ex.Message}). Kopia: {bak}. " + "Wczytano domyślne w pamięci i NIE nadpisano pliku — popraw i przeładuj."); } } catch { PrintError("[Config] Uszkodzony — wczytano domyślne (kopia się nie udała)."); } // celowo BEZ SaveConfig() } } protected override void SaveConfig() => Config.WriteObject(_cfg); // ── Lifecycle ──────────────────────────────────────────────── void OnServerInitialized() { _serverId = LoadOrCreateServerId(); FetchPublicIp(); timer.Once(12f, SendHeartbeat); _timer = timer.Every(Interval, SendHeartbeat); } void Unload() => _timer?.Destroy(); // ── Heartbeat ──────────────────────────────────────────────── // Stała tożsamość w pliku danych (oxide/data) — poza configiem. // Regeneracja TYLKO przez usunięcie oxide/data/MajestatServerId.json. // Wspólny plik tożsamości dla wszystkich wariantów pluginu (Core/Net/…). private const string IdDataFile = "MajestatServerId"; private const string IdLegacyCore = "MajestatTopCore_id"; private const string IdLegacyNet = "MajestatTopNet_id"; private string ReadIdFile(string name) { try { var d = Interface.Oxide.DataFileSystem.ReadObject>(name); if (d != null && d.TryGetValue("serverId", out var v) && !string.IsNullOrEmpty(v)) return v; } catch { } return null; } private string LoadOrCreateServerId() { string id = ReadIdFile(IdDataFile); if (string.IsNullOrEmpty(id)) { // migracja: wspólny pusty → stary plik Core → stary plik Net → legacy z configu id = ReadIdFile(IdLegacyCore); if (string.IsNullOrEmpty(id)) id = ReadIdFile(IdLegacyNet); if (string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(_legacyServerId)) id = _legacyServerId; if (string.IsNullOrEmpty(id)) id = Guid.NewGuid().ToString("N"); try { Interface.Oxide.DataFileSystem.WriteObject(IdDataFile, new Dictionary { ["serverId"] = id }); } catch { } Puts($"[Net] Server ID: {id}"); } return id; } private void FetchPublicIp() { webrequest.Enqueue("https://api.ipify.org", null, (code, body) => { if (code == 200 && !string.IsNullOrEmpty(body)) { _publicIp = body.Trim(); Puts($"[Net] Publiczny IP: {_publicIp}"); } else Puts("[Net] Nie udało się wykryć IP — Master użyje adresu z połączenia."); }, this); } private void SendHeartbeat() { if (string.IsNullOrEmpty(_serverId)) _serverId = LoadOrCreateServerId(); int gamePort = _cfg.GamePort > 0 ? _cfg.GamePort : ConVar.Server.port; int queryPort = ConVar.Server.queryport > 0 ? ConVar.Server.queryport : gamePort; // Typ z ustawień serwera: PvE flaga → "pve", inaczej "pvp" (Net = lekki PvP). string type = ConVar.Server.pve ? "pve" : "pvp"; // Data ostatniego wipe'u = czas utworzenia bieżącego sava mapy. long wipedAt = 0; try { var t = SaveRestore.SaveCreatedTime; if (t > new DateTime(2000, 1, 1)) wipedAt = ((DateTimeOffset)t.ToUniversalTime()).ToUnixTimeSeconds(); } catch { } var payload = new Dictionary { ["serverId"] = _serverId, ["token"] = _cfg.Token ?? "", ["client"] = Name, // "MajestatTopNet" → Master skróci do "Net" ["version"] = Build, ["name"] = ConVar.Server.hostname, ["description"]= ConVar.Server.description, ["tags"] = ConVar.Server.tags, ["headerImage"]= ConVar.Server.headerimage, ["url"] = ConVar.Server.url, ["pve"] = ConVar.Server.pve, ["official"] = ConVar.Server.official, ["players"] = BasePlayer.activePlayerList.Count, ["maxPlayers"] = ConVar.Server.maxplayers, ["map"] = World.Name, ["worldSize"] = World.Size, ["seed"] = World.Seed, ["uptime"] = (int)UnityEngine.Time.realtimeSinceStartup, ["ip"] = _publicIp, ["gamePort"] = gamePort, ["queryPort"] = queryPort, ["webPort"] = 0, ["hasWeb"] = false, ["webUrl"] = "", ["type"] = type, ["wipe"] = "", // Rust nie ma tego w ustawieniach — Net nie raportuje ["wipedAt"] = wipedAt, // data ostatniego wipe'u (unix) }; string body = JsonConvert.SerializeObject(payload); var headers = new Dictionary { ["Content-Type"] = "application/json" }; webrequest.Enqueue(MasterUrl, body, (code, response) => { if (code == 200) { Puts("[Net] Heartbeat OK."); HandlePremiumResponse(response); } else if (code == 0) Puts("[Net] Master nieosiągalny (timeout / brak sieci)."); else PrintWarning($"[Net] Master odrzucił ({code}): {response}"); }, this, RequestMethod.POST, headers, 10f); } private int _premiumState = -1; // -1 nieznany, 0 brak, 1 aktywny, 2 wygasły private void HandlePremiumResponse(string response) { try { var data = JsonConvert.DeserializeObject>(response); if (data == null) return; bool premium = data.ContainsKey("premium") && Convert.ToBoolean(data["premium"]); bool expired = data.ContainsKey("premiumExpired") && Convert.ToBoolean(data["premiumExpired"]); int state = premium ? 1 : (expired ? 2 : 0); if (state == _premiumState) return; _premiumState = state; if (state == 1) { string until = ""; if (data.ContainsKey("premiumUntil") && data["premiumUntil"] != null) { long ts = Convert.ToInt64(data["premiumUntil"]); until = " (do " + DateTimeOffset.FromUnixTimeSeconds(ts) .LocalDateTime.ToString("yyyy-MM-dd") + ")"; } Puts("[Net] Subskrypcja premium aktywna" + until + "."); } else if (state == 2) PrintWarning("[Net] Subskrypcja premium straciła ważność."); } catch { } } } }