using System; using System.Collections.Generic; using Newtonsoft.Json; using UnityEngine; namespace Oxide.Plugins { [Info("NodeSpawnRates", "Uzumi", "1.0.0")] [Description("Reduce global ore spawn rates for ore-stone, ore-metal, and ore-sulfur.")] internal class NodeSpawnRates : RustPlugin { #region Configuration private PluginConfig _config; private sealed class PluginConfig { [JsonProperty("Enabled")] public bool Enabled = true; [JsonProperty("Ore-stone multiplier (0+). 1=default, 0.5=half, 2=double (does not instantly add nodes)")] public float OreStone = 1f; [JsonProperty("Ore-metal multiplier (0+). 1=default, 0.5=half, 2=double (does not instantly add nodes)")] public float OreMetal = 1f; [JsonProperty("Ore-sulfur multiplier (0+). 1=default, 0.5=half, 2=double (does not instantly add nodes)")] public float OreSulfur = 1f; } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) throw new Exception("Config returned null"); } catch (Exception e) { PrintWarning($"Config error, loading defaults. {e.Message}"); LoadDefaultConfig(); } EnsureConfigIntegrity(); SaveConfig(); } protected override void LoadDefaultConfig() { _config = new PluginConfig(); SaveConfig(); } private new void SaveConfig() => Config.WriteObject(_config, true); private void EnsureConfigIntegrity() { if (_config == null) _config = new PluginConfig(); _config.OreStone = Mathf.Max(0f, _config.OreStone); _config.OreMetal = Mathf.Max(0f, _config.OreMetal); _config.OreSulfur = Mathf.Max(0f, _config.OreSulfur); } #endregion #region Fields private const string PermAdmin = "nodespawnrates.admin"; #endregion #region Init, permissions, update check // ─── Update check ──────────────────────────────────────────────────────────── private const string UpdateCheckUrl = "https://nordicrust.eu/plugin-version.php?slug=nodespawnrates"; private const string UpdatePageUrl = "https://nordicrust.eu/plugins"; private bool _updateCheckScheduled; private void Init() { permission.RegisterPermission(PermAdmin, this); ScheduleUpdateCheck(); } private void OnServerInitialized() { ScheduleUpdateCheck(); ApplyGlobalCulls(); } private void ScheduleUpdateCheck() { if (_updateCheckScheduled) return; _updateCheckScheduled = true; timer.Once(8f, CheckForPluginUpdate); } private void CheckForPluginUpdate() { webrequest.Enqueue(UpdateCheckUrl, null, (code, response) => { if (code != 200 || string.IsNullOrWhiteSpace(response)) return; string latestVersion = null; string updateUrl = UpdatePageUrl; var lines = response.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var idx = line.IndexOf('='); if (idx <= 0) continue; var key = line.Substring(0, idx).Trim(); var value = line.Substring(idx + 1).Trim(); if (key.Equals("latest_version", StringComparison.OrdinalIgnoreCase)) latestVersion = value; else if (key.Equals("download_url", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrEmpty(value)) updateUrl = value; } if (string.IsNullOrEmpty(latestVersion)) return; var currentVersion = Version.ToString(); if (IsRemoteVersionNewer(currentVersion, latestVersion)) { PrintWarning($"Update available for {Name}: current {currentVersion}, latest {latestVersion}. Download: {updateUrl}"); } }, this); } private static bool IsRemoteVersionNewer(string currentVersion, string remoteVersion) { var current = ParseLooseVersion(currentVersion); var remote = ParseLooseVersion(remoteVersion); return remote.CompareTo(current) > 0; } private static System.Version ParseLooseVersion(string value) { if (string.IsNullOrWhiteSpace(value)) return new System.Version(0, 0, 0); var core = value.Trim(); var dash = core.IndexOf('-'); if (dash >= 0) core = core.Substring(0, dash); var plus = core.IndexOf('+'); if (plus >= 0) core = core.Substring(0, plus); System.Version parsed; return System.Version.TryParse(core, out parsed) ? parsed : new System.Version(0, 0, 0); } // ───────────────────────────────────────────────────────────────────────────── #endregion #region Commands [ChatCommand("nodespawn")] private void CmdNodeSpawn(BasePlayer player, string command, string[] args) { if (player == null) return; if (!permission.UserHasPermission(player.UserIDString, PermAdmin)) { SendReply(player, "You do not have permission to use this command."); return; } if (args == null || args.Length == 0) { SendReply(player, "Usage: /ns list | /ns set | /ns get "); return; } string sub = args[0].ToLowerInvariant(); if (sub == "list") { var counts = GetGlobalOreCounts(); SendReply(player, $"Ore counts: ore-stone={counts.Stone}, ore-metal={counts.Metal}, ore-sulfur={counts.Sulfur}"); SendReply(player, $"Multipliers: ore-stone={_config.OreStone:0.###}, ore-metal={_config.OreMetal:0.###}, ore-sulfur={_config.OreSulfur:0.###}"); return; } if (sub == "get") { if (args.Length < 2) { SendReply(player, "Usage: /ns get "); return; } string key = NormalizeKey(args[1]); float mult = GetMultiplier(key); if (mult < 0f) { SendReply(player, "Unknown key. Use: ore-stone, ore-metal, ore-sulfur"); return; } SendReply(player, $"Multiplier for {key}: {mult:0.###}"); return; } if (sub == "set") { if (args.Length < 3) { SendReply(player, "Usage: /ns set "); return; } string key = NormalizeKey(args[1]); float multiplier; if (!float.TryParse(args[2], out multiplier)) { SendReply(player, "Invalid multiplier. Example: 0.5"); return; } multiplier = Mathf.Max(0f, multiplier); if (_config == null) return; if (!SetMultiplier(key, multiplier)) { SendReply(player, "Unknown key. Use: ore-stone, ore-metal, ore-sulfur"); return; } SaveConfig(); CullExistingToMultiplier(key, multiplier); SendReply(player, $"Set {key} multiplier to {multiplier:0.###}."); return; } SendReply(player, "Unknown subcommand. Use: /ns list | get | set"); } [ChatCommand("ns")] private void CmdNodeSpawnAlias(BasePlayer player, string command, string[] args) => CmdNodeSpawn(player, command, args); #endregion #region Hooks private void OnEntitySpawned(BaseNetworkable entity) { if (_config == null || !_config.Enabled) return; var ore = entity as OreResourceEntity; if ((object)ore == null) return; string shortName = ore.ShortPrefabName; string key = GetKeyForNodeShortName(shortName); if (string.IsNullOrEmpty(key)) return; float mult = GetMultiplier(key); if (mult >= 1f) return; if (mult <= 0f || UnityEngine.Random.value > mult) { var ent = ore as BaseEntity; if ((object)ent == null) return; NextTick(() => { if ((object)ent == null || ent.IsDestroyed) return; ent.Kill(); }); } } #endregion #region Global counts, multipliers, and culling private struct OreCounts { public int Stone; public int Metal; public int Sulfur; } private OreCounts GetGlobalOreCounts() { var counts = new OreCounts(); try { foreach (var net in BaseNetworkable.serverEntities) { var ore = net as OreResourceEntity; if ((object)ore == null) continue; var key = GetKeyForNodeShortName(ore.ShortPrefabName); if (key == "ore-stone") counts.Stone++; else if (key == "ore-metal") counts.Metal++; else if (key == "ore-sulfur") counts.Sulfur++; } } catch { } return counts; } private void ApplyGlobalCulls() { if (_config == null || !_config.Enabled) return; CullExistingToMultiplier("ore-stone", _config.OreStone); CullExistingToMultiplier("ore-metal", _config.OreMetal); CullExistingToMultiplier("ore-sulfur", _config.OreSulfur); } private void CullExistingToMultiplier(string key, float multiplier) { if (_config == null || !_config.Enabled) return; if (multiplier >= 1f) return; var list = new List(); try { foreach (var net in BaseNetworkable.serverEntities) { var ore = net as OreResourceEntity; if ((object)ore == null) continue; if (!string.Equals(GetKeyForNodeShortName(ore.ShortPrefabName), key, StringComparison.OrdinalIgnoreCase)) continue; var ent = ore as BaseEntity; if ((object)ent != null && !ent.IsDestroyed) list.Add(ent); } } catch { return; } int total = list.Count; int target = Mathf.Clamp(Mathf.RoundToInt(total * Mathf.Max(0f, multiplier)), 0, total); int toRemove = total - target; if (toRemove <= 0) return; Shuffle(list); for (int i = 0; i < toRemove; i++) { var ent = list[i]; if ((object)ent == null || ent.IsDestroyed) continue; ent.Kill(); } } private static void Shuffle(IList list) { for (int i = list.Count - 1; i > 0; i--) { int j = UnityEngine.Random.Range(0, i + 1); T tmp = list[i]; list[i] = list[j]; list[j] = tmp; } } private string GetKeyForNodeShortName(string shortName) { if (string.IsNullOrEmpty(shortName)) return null; if (shortName.IndexOf("sulfur", StringComparison.OrdinalIgnoreCase) >= 0) return "ore-sulfur"; if (shortName.IndexOf("metal", StringComparison.OrdinalIgnoreCase) >= 0) return "ore-metal"; if (shortName.IndexOf("stone", StringComparison.OrdinalIgnoreCase) >= 0) return "ore-stone"; return null; } private string NormalizeKey(string raw) { if (string.IsNullOrWhiteSpace(raw)) return ""; string key = raw.Trim(); if (key.Equals("stone", StringComparison.OrdinalIgnoreCase)) return "ore-stone"; if (key.Equals("metal", StringComparison.OrdinalIgnoreCase)) return "ore-metal"; if (key.Equals("sulfur", StringComparison.OrdinalIgnoreCase)) return "ore-sulfur"; if (key.Equals("ore-stone", StringComparison.OrdinalIgnoreCase)) return "ore-stone"; if (key.Equals("ore-metal", StringComparison.OrdinalIgnoreCase)) return "ore-metal"; if (key.Equals("ore-sulfur", StringComparison.OrdinalIgnoreCase)) return "ore-sulfur"; return key.ToLowerInvariant(); } private float GetMultiplier(string key) { if (_config == null) return 1f; if (key == "ore-stone") return Mathf.Max(0f, _config.OreStone); if (key == "ore-metal") return Mathf.Max(0f, _config.OreMetal); if (key == "ore-sulfur") return Mathf.Max(0f, _config.OreSulfur); return -1f; } private bool SetMultiplier(string key, float value) { if (_config == null) return false; value = Mathf.Max(0f, value); if (key == "ore-stone") { _config.OreStone = value; return true; } if (key == "ore-metal") { _config.OreMetal = value; return true; } if (key == "ore-sulfur") { _config.OreSulfur = value; return true; } return false; } #endregion } }