using System; using System.Collections.Generic; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries; using Oxide.Core.Plugins; namespace Oxide.Plugins { [Info("MajestatTopNet", "Wo0t", "1.0.3")] [Description("Lightweight reporter - reports the server to Majestat Network (client: Net). No dependencies.")] public class MajestatTopNet : RustPlugin { private static readonly string MasterUrl = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String( "aHR0cHM6Ly9tYXN0ZXIubWFqZXN0YXRuZXR3b3JrLmtvbGRyaXguY29tL2FwaS9oZWFydGJlYXQucGhw")); private static readonly string UpdateUrl = System.Text.Encoding.UTF8.GetString( System.Convert.FromBase64String( "aHR0cDovL2Rvd25sb2FkLmtvbGRyaXguY29tL3J1c3QvcGx1Z2lucy9NYWplc3RhdFRvcE5ldC92ZXJzaW9uLmpzb24=")); private const float Interval = 60f; private const string Build = "1.0.3"; private ConfigData _cfg; private Timer _timer; private Timer _updateTimer; private bool _active = false; private string _publicIp = ""; private string _serverId = ""; private string _legacyServerId = null; // -- Config ----------------------------------------- private class ConfigData { // Verification token - issued in the Master panel; gives the verified 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("empty config"); SaveConfig(); // adds new fields, KEEPS existing values } catch (Exception ex) { _cfg = new ConfigData(); 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] Corrupt ({ex.Message}). Backup: {bak}. " + "Loaded defaults in memory and did NOT overwrite the file - fix it and reload."); } } catch { PrintError("[Config] Corrupt - loaded defaults (backup failed)."); } } } protected override void SaveConfig() => Config.WriteObject(_cfg); // -- Lifecycle ------------------------------------------------ // Net and Core must not both report. If Core is present, Net stays // inactive (Core handles server reporting). Net (re)activates only // when Core is absent. void OnServerInitialized() { if (plugins.Find("MajestatTopCore") != null) { PrintWarning("[Net] MajestatTopCore detected - Net stays INACTIVE (Core handles reporting)."); return; } Activate(); } void OnPluginLoaded(Plugin plugin) { if (plugin != null && plugin.Name == "MajestatTopCore" && _active) Deactivate(); } void OnPluginUnloaded(Plugin plugin) { // Core may just be reloading - wait, then reactivate only if it's really gone. if (plugin != null && plugin.Name == "MajestatTopCore" && !_active) { timer.Once(15f, () => { if (!_active && plugins.Find("MajestatTopCore") == null) { PrintWarning("[Net] MajestatTopCore removed - Net REACTIVATED."); Activate(); } }); } } void Unload() { _timer?.Destroy(); _updateTimer?.Destroy(); } private void Activate() { if (_active) return; _active = true; if (string.IsNullOrEmpty(_serverId)) _serverId = LoadOrCreateServerId(); FetchPublicIp(); _timer?.Destroy(); timer.Once(2f, SendHeartbeat); _timer = timer.Every(Interval, SendHeartbeat); _updateTimer?.Destroy(); timer.Once(15f, CheckForUpdate); _updateTimer = timer.Every(21600f, CheckForUpdate); } private void Deactivate() { _active = false; _timer?.Destroy(); _timer = null; _updateTimer?.Destroy(); _updateTimer = null; PrintWarning("[Net] MajestatTopCore detected - Net DEACTIVATED (Core handles reporting)."); } // -- Update checker ------------------------------------------- private void CheckForUpdate() { if (!_active) return; webrequest.Enqueue(UpdateUrl, null, (code, response) => { if (code != 200 || string.IsNullOrEmpty(response)) { Puts("[Update] Update check failed - update server unavailable."); return; } try { var data = JsonConvert.DeserializeObject>(response); string latest = (data != null && data.ContainsKey("version")) ? data["version"] : null; if (latest != null && IsNewerVersion(latest, Build)) PrintWarning($"[Update] Newer version available ({latest}) - your version ({Build})."); else Puts($"[Update] Up to date ({Build})."); } catch { Puts("[Update] Error reading update response."); } }, this); } 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 = Array.ConvertAll(lc.Split('.'), int.Parse); var c = Array.ConvertAll(cc.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; } } // -- Heartbeat ------------------------------------------------ // Persistent identity in a data file (oxide/data on Oxide, carbon/data on Carbon), // outside the config. Regenerate ONLY by deleting MajestatServerId.json. // Shared identity file across all plugin variants (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)) { // migration: shared empty -> old Core file -> old Net file -> legacy config 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] Public IP: {_publicIp}"); } else Puts("[Net] Could not detect public IP - Master will use the connection address."); }, this); } private void SendHeartbeat() { if (!_active) return; 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; string type = ConVar.Server.pve ? "pve" : "pvp"; 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, ["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"] = "", ["wipedAt"] = wipedAt, }; 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 unreachable (timeout / no network)."); else PrintWarning($"[Net] Master rejected ({code}): {response}"); }, this, RequestMethod.POST, headers, 10f); } private int _premiumState = -1; // -1 unknown, 0 none, 1 active, 2 expired 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 = " (until " + DateTimeOffset.FromUnixTimeSeconds(ts) .LocalDateTime.ToString("yyyy-MM-dd") + ")"; } Puts("[Net] Premium subscription active" + until + "."); } else if (state == 2) PrintWarning("[Net] Premium subscription expired."); } catch { } } } }