C#:
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using Oxide.Core;
using Network;
namespace Oxide.Plugins
{
[Info("Ore Ecology", "Quen", "1.1.1")]
[Description("Balances ore node density per biome, with multipliers per ore type. With cleanup & admin stats command (in chat).")]
public class OreEcology : RustPlugin
{
private ConfigData config;
private StoredData data;
private class StoredData
{
public HashSet<ulong> SpawnedNodeNetIDs = new HashSet<ulong>();
}
#region Hooks
private void OnServerInitialized()
{
LoadData();
CleanupInvalidNodes();
AdjustBiomeNodes();
timer.Every(config.CheckIntervalMinutes * 60f, AdjustBiomeNodes);
permission.RegisterPermission("oreecology.admin", this);
}
private void Unload()
{
RemoveSpawnedNodes();
SaveData();
}
#endregion
#region Commands
[ChatCommand("oreeco")]
private void CmdOreEco(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, "oreecology.admin"))
{
SendReply(player, "<color=#ff6666>You do not have permission to use this command.</color>");
return;
}
var stats = GetBiomeStatsString();
SendReply(player, "<color=#ffd700>=== Ore Ecology Current Node Counts ===</color>");
foreach (var line in stats)
{
SendReply(player, line);
}
// Removed the bottom divider as requested
}
#endregion
#region Config
private class ConfigData
{
public Dictionary<string, Dictionary<string, float>> BiomeMultipliers;
public int MaxExtraNodesPerBiome;
public float CheckIntervalMinutes;
public float MaxSlope;
public float BiomeThreshold;
public bool RemoveOnUnload;
}
protected override void LoadDefaultConfig()
{
config = new ConfigData
{
BiomeMultipliers = new Dictionary<string, Dictionary<string, float>>
{
{ "Arid", new Dictionary<string, float>
{
{ "Metal", 1.5f },
{ "Sulfur", 1.0f },
{ "Stone", 0.8f }
}
},
{ "Temperate", new Dictionary<string, float>
{
{ "Metal", 1.0f },
{ "Sulfur", 1.0f },
{ "Stone", 1.0f }
}
},
{ "Tundra", new Dictionary<string, float>
{
{ "Metal", 0.9f },
{ "Sulfur", 1.3f },
{ "Stone", 1.1f }
}
},
{ "Arctic", new Dictionary<string, float>
{
{ "Metal", 0.7f },
{ "Sulfur", 2.0f },
{ "Stone", 1.2f }
}
}
},
MaxExtraNodesPerBiome = 100,
CheckIntervalMinutes = 5f,
MaxSlope = 35f,
BiomeThreshold = 0.6f,
RemoveOnUnload = true
};
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
config = Config.ReadObject<ConfigData>();
if (config == null || config.BiomeMultipliers == null)
{
PrintWarning("Invalid config detected. Regenerating...");
LoadDefaultConfig();
}
}
protected override void SaveConfig()
{
Config.WriteObject(config, true);
}
#endregion
#region Data Management
private void LoadData()
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
if (data == null)
data = new StoredData();
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
#endregion
#region Data
private readonly Dictionary<string, string> orePrefabs = new Dictionary<string, string>
{
{ "Metal", "assets/bundled/prefabs/autospawn/resource/ores/metal-ore.prefab" },
{ "Sulfur", "assets/bundled/prefabs/autospawn/resource/ores/sulfur-ore.prefab" },
{ "Stone", "assets/bundled/prefabs/autospawn/resource/ores/stone-ore.prefab" }
};
private const int BiomeArid = 1;
private const int BiomeTemperate = 2;
private const int BiomeTundra = 4;
private const int BiomeArctic = 8;
#endregion
#region Core Logic
private void AdjustBiomeNodes()
{
var nodes = UnityEngine.Object.FindObjectsOfType<ResourceEntity>();
var biomeCounts = new Dictionary<string, Dictionary<string, int>>();
foreach (var node in nodes)
{
if (node == null || node.ShortPrefabName == null) continue;
string oreType = GetOreType(node.ShortPrefabName);
if (oreType == null) continue;
string biome = GetBiomeName(node.transform.position);
if (biome == "Unknown") continue;
if (!biomeCounts.ContainsKey(biome))
{
biomeCounts[biome] = new Dictionary<string, int> { { "Metal", 0 }, { "Sulfur", 0 }, { "Stone", 0 } };
}
biomeCounts[biome][oreType]++;
}
foreach (var biomeKey in config.BiomeMultipliers.Keys)
{
int biomeInt = BiomeNameToInt(biomeKey);
if (biomeInt == 0) continue;
foreach (var ore in orePrefabs.Keys)
{
int current = biomeCounts.ContainsKey(biomeKey) ? biomeCounts[biomeKey][ore] : 0;
float multiplier = config.BiomeMultipliers[biomeKey][ore];
int target = Mathf.RoundToInt(current * multiplier);
int toSpawn = Mathf.Clamp(target - current, 0, config.MaxExtraNodesPerBiome);
if (toSpawn > 0)
{
int spawned = SpawnNodes(biomeKey, biomeInt, ore, toSpawn);
if (spawned > 0)
{
Puts($"Biome [{biomeKey}] → Spawned {spawned}x {ore} nodes.");
}
}
}
}
}
private int SpawnNodes(string biomeName, int biomeInt, string oreType, int count)
{
string prefab = orePrefabs[oreType];
int spawned = 0;
for (int i = 0; i < count * 3 && spawned < count; i++)
{
Vector3 pos = FindValidPos(biomeInt);
if (pos == Vector3.zero) continue;
var entity = GameManager.server.CreateEntity(prefab, pos);
if (entity == null) continue;
entity.Spawn();
spawned++;
data.SpawnedNodeNetIDs.Add(entity.net.ID.Value);
}
if (spawned < count)
{
Puts($"Warning: Only spawned {spawned}/{count} {oreType} nodes in {biomeName} (limited valid positions).");
}
SaveData();
return spawned;
}
private Vector3 FindValidPos(int biomeInt)
{
int attempts = 80;
for (int i = 0; i < attempts; i++)
{
Vector3 pos = new Vector3(
UnityEngine.Random.Range(-TerrainMeta.Size.x / 2 + 100f, TerrainMeta.Size.x / 2 - 100f),
0,
UnityEngine.Random.Range(-TerrainMeta.Size.z / 2 + 100f, TerrainMeta.Size.z / 2 - 100f)
);
pos.y = TerrainMeta.HeightMap.GetHeight(pos);
float strength = TerrainMeta.BiomeMap.GetBiome(pos, biomeInt);
if (strength < config.BiomeThreshold) continue;
var normal = TerrainMeta.HeightMap.GetNormal(pos);
float slope = Vector3.Angle(Vector3.up, normal);
if (slope > config.MaxSlope) continue;
if (AntiHack.TestInsideTerrain(pos)) continue;
return pos;
}
return Vector3.zero;
}
#endregion
#region Stats
private List<string> GetBiomeStatsString()
{
var lines = new List<string>();
var nodes = UnityEngine.Object.FindObjectsOfType<ResourceEntity>();
var biomeCounts = new Dictionary<string, Dictionary<string, int>>();
foreach (var node in nodes)
{
if (node == null || node.ShortPrefabName == null) continue;
string oreType = GetOreType(node.ShortPrefabName);
if (oreType == null) continue;
string biome = GetBiomeName(node.transform.position);
if (biome == "Unknown") continue;
if (!biomeCounts.ContainsKey(biome))
{
biomeCounts[biome] = new Dictionary<string, int> { { "Metal", 0 }, { "Sulfur", 0 }, { "Stone", 0 } };
}
biomeCounts[biome][oreType]++;
}
foreach (var biome in config.BiomeMultipliers.Keys)
{
lines.Add($"<color=#ffaa00>Biome: {biome}</color>");
foreach (var ore in orePrefabs.Keys)
{
int count = biomeCounts.ContainsKey(biome) ? biomeCounts[biome][ore] : 0;
float mult = config.BiomeMultipliers[biome][ore];
int baseApprox = mult > 0 ? Mathf.RoundToInt(count / mult) : 0;
lines.Add($" <color=#ffffff>{ore}:</color> <color=#88ff88>{count}</color> (mult: <color=#ffff66>{mult}x</color>, base ~<color=#66ccff>{baseApprox}</color>)");
}
}
return lines;
}
#endregion
#region Cleanup
private void RemoveSpawnedNodes()
{
if (!config.RemoveOnUnload) return;
int removed = 0;
foreach (var netIdValue in data.SpawnedNodeNetIDs.ToList())
{
var entity = BaseNetworkable.serverEntities.Find(new NetworkableId { Value = netIdValue }) as BaseEntity;
if (entity != null && !entity.IsDestroyed)
{
entity.Kill();
removed++;
}
}
Puts($"Removed {removed} plugin-spawned ore nodes on unload/reload.");
data.SpawnedNodeNetIDs.Clear();
SaveData();
}
private void CleanupInvalidNodes()
{
int cleaned = 0;
var toRemove = new List<ulong>();
foreach (var netIdValue in data.SpawnedNodeNetIDs)
{
var entity = BaseNetworkable.serverEntities.Find(new NetworkableId { Value = netIdValue });
if (entity == null || entity.IsDestroyed)
{
toRemove.Add(netIdValue);
cleaned++;
}
}
foreach (var id in toRemove)
{
data.SpawnedNodeNetIDs.Remove(id);
}
if (cleaned > 0)
{
Puts($"Cleaned up {cleaned} invalid/missing tracked node IDs.");
}
}
#endregion
#region Helpers
private string GetOreType(string shortPrefab)
{
if (shortPrefab.Contains("metal-ore")) return "Metal";
if (shortPrefab.Contains("sulfur-ore")) return "Sulfur";
if (shortPrefab.Contains("stone-ore")) return "Stone";
return null;
}
private string GetBiomeName(Vector3 pos)
{
float arid = TerrainMeta.BiomeMap.GetBiome(pos, BiomeArid);
float temp = TerrainMeta.BiomeMap.GetBiome(pos, BiomeTemperate);
float tund = TerrainMeta.BiomeMap.GetBiome(pos, BiomeTundra);
float arct = TerrainMeta.BiomeMap.GetBiome(pos, BiomeArctic);
float max = Mathf.Max(arid, Mathf.Max(temp, Mathf.Max(tund, arct)));
if (max < config.BiomeThreshold) return "Unknown";
if (arid == max) return "Arid";
if (temp == max) return "Temperate";
if (tund == max) return "Tundra";
if (arct == max) return "Arctic";
return "Unknown";
}
private int BiomeNameToInt(string name)
{
switch (name)
{
case "Arid": return BiomeArid;
case "Temperate": return BiomeTemperate;
case "Tundra": return BiomeTundra;
case "Arctic": return BiomeArctic;
default: return 0;
}
}
#endregion
}
}