using System; using System.Linq; using System.Collections.Generic; using Oxide.Core; using UnityEngine; namespace Oxide.Plugins { [Info("PlantBiomeCustomizer", "TungstonMiner", "1.0.0")] [Description("Customize placement of plants around the map by biome")] public class PlantBiomeTuner : RustPlugin { // Config ////////////////////////////////////////////////////////////////////////////////// PluginConfig configData; public class PluginConfig { // Key = arbitrary rule name (e.g., "berry_blue", "pumpkins_desert", etc.), value = rule definition. public Dictionary rules = new Dictionary(); public static PluginConfig Default() { return new PluginConfig(); } } public class PlantRule { public bool enabled = true; public string[] prefabFilters = Array.Empty(); // case-insensitive substrings matched against population identity public string[] biomes = Array.Empty(); // names matching TerrainMeta.BiomeType flags public float densityMultiplier = 1.0f; // multiply existing density/weights } protected override void LoadDefaultConfig() { configData = PluginConfig.Default(); SaveConfig(); } protected override void SaveConfig() { Config.WriteObject(configData, true); } protected override void LoadConfig() { base.LoadConfig(); try { configData = Config.ReadObject(); } catch { PrintError("Config read error; generating new default config."); configData = PluginConfig.Default(); } SaveConfig(); } // Hooks /////////////////////////////////////////////////////////////////////////////////// void OnServerInitialized(bool initial) { NextTick(ApplyAll); } // Commands //////////////////////////////////////////////////////////////////////////////// [ConsoleCommand("plantbiometuner.apply")] void CmdApply(ConsoleSystem.Arg arg) { if (!arg.IsAdmin) return; ApplyAll(); arg.ReplyWith("PlantBiomeTuner: applied settings and refilled populations."); } // Core Logic ////////////////////////////////////////////////////////////////////////////// void ApplyAll() { int changed = 0; int matched = 0; var populations = Resources.FindObjectsOfTypeAll(); if (populations == null || populations.Length == 0) { PrintWarning("No SpawnPopulation objects found (map not ready yet?). Will retry next tick."); NextTick(ApplyAll); return; } if (configData.rules == null || configData.rules.Count == 0) { Puts("PlantBiomeTuner: no rules configured."); return; } foreach (var kv in configData.rules) { string ruleKey = kv.Key; PlantRule rule = kv.Value; if (rule == null || !rule.enabled) continue; var filters = (rule.prefabFilters ?? Array.Empty()) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => s.Trim()) .ToArray(); if (filters.Length == 0) continue; int biomeMask = BuildBiomeMask(rule.biomes); if (biomeMask == 0) { PrintWarning($"[{ruleKey}] Biome mask is empty; skipping."); continue; } foreach (var population in populations) { if (population == null) continue; string id = GetPopulationIdentity(population); if (string.IsNullOrEmpty(id)) continue; if (!filters.Any(f => id.IndexOf(f, StringComparison.OrdinalIgnoreCase) >= 0)) continue; matched++; try { bool anyChange = false; anyChange |= TrySetBiomeMask(population, biomeMask); anyChange |= TrySetDensity(population, rule.densityMultiplier); if (anyChange) { changed++; Puts($"[{ruleKey}] Updated '{id}' (biomes={string.Join(",", rule.biomes)}, density x{rule.densityMultiplier:0.##})"); } } catch (Exception e) { PrintWarning($"[{ruleKey}] Failed to update '{id}': {e.Message}"); } } } try { ConsoleSystem.Run(ConsoleSystem.Option.Server, "spawn.delete_populations"); ConsoleSystem.Run(ConsoleSystem.Option.Server, "spawn.fill_populations"); } catch (Exception e) { PrintWarning($"Failed to refill populations: {e.Message}"); } Puts($"PlantBiomeTuner: matched {matched} populations; applied changes to {changed}."); } int BuildBiomeMask(IEnumerable names) { if (names == null) return 0; int mask = 0; foreach (var n in names) { if (string.IsNullOrWhiteSpace(n)) continue; try { var value = (TerrainMeta.BiomeType)Enum.Parse(typeof(TerrainMeta.BiomeType), n.Trim(), true); mask |= (int)value; // combine provided flags; no manual shifts } catch { PrintWarning($"Unknown biome '{n}'. Ensure names match TerrainMeta.BiomeType."); } } return mask; } string GetPopulationIdentity(SpawnPopulation population) { try { var res = population.Resource; if (res != null) { var prefab = res.prefab; if (prefab != null) { var path = prefab.resourcePath; if (!string.IsNullOrEmpty(path)) return path; } } } catch { // ignore } return population.name ?? population.GetType().Name; } bool TrySetBiomeMask(SpawnPopulation population, int biomeMask) { bool changed = false; var field = population.GetType().GetField("BiomePreference") ?? population.GetType().GetField("TopologyPreference"); if (field != null && field.FieldType == typeof(int)) { int current = (int) field.GetValue(population); if (current != biomeMask) { field.SetValue(population, biomeMask); changed = true; } return changed; } var prop = population.GetType().GetProperty("BiomePreference"); if (prop != null && prop.PropertyType == typeof(int) && prop.CanWrite) { int current = (int) (prop.GetValue(population) ?? 0); if (current != biomeMask) { prop.SetValue(population, biomeMask, null); changed = true; } return changed; } return false; } bool TrySetDensity(SpawnPopulation population, float multiplier) { if (Mathf.Approximately(multiplier, 1f)) return false; bool changed = false; var densityField = population.GetType().GetField("Density"); if (densityField != null && densityField.FieldType == typeof(float)) { float d = (float)densityField.GetValue(population); float nd = Mathf.Max(0f, d * multiplier); if (!Mathf.Approximately(d, nd)) { densityField.SetValue(population, nd); changed = true; } } var sptField = population.GetType().GetField("SpawnPerTick"); if (sptField != null && sptField.FieldType == typeof(float)) { float v = (float)sptField.GetValue(population); float nv = Mathf.Max(0f, v * multiplier); if (!Mathf.Approximately(v, nv)) { sptField.SetValue(population, nv); changed = true; } } var clusterField = population.GetType().GetField("ClusterDensities"); if (clusterField != null && typeof(float[]).IsAssignableFrom(clusterField.FieldType)) { var arr = (float[])clusterField.GetValue(population); if (arr != null && arr.Length > 0) { for (int i = 0; i < arr.Length; i++) arr[i] = Mathf.Max(0f, arr[i] * multiplier); changed = true; } } return changed; } } }